e11y 0.1.0 → 0.2.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.
Files changed (70) hide show
  1. checksums.yaml +4 -4
  2. data/.rspec +1 -0
  3. data/.rubocop.yml +20 -0
  4. data/CHANGELOG.md +151 -13
  5. data/README.md +1138 -104
  6. data/RELEASE.md +254 -0
  7. data/Rakefile +377 -0
  8. data/benchmarks/OPTIMIZATION.md +246 -0
  9. data/benchmarks/README.md +103 -0
  10. data/benchmarks/allocation_profiling.rb +253 -0
  11. data/benchmarks/e11y_benchmarks.rb +447 -0
  12. data/benchmarks/ruby_baseline_allocations.rb +175 -0
  13. data/benchmarks/run_all.rb +9 -21
  14. data/docs/00-ICP-AND-TIMELINE.md +2 -2
  15. data/docs/ADR-001-architecture.md +1 -1
  16. data/docs/ADR-004-adapter-architecture.md +247 -0
  17. data/docs/ADR-009-cost-optimization.md +231 -115
  18. data/docs/ADR-017-multi-rails-compatibility.md +103 -0
  19. data/docs/ADR-INDEX.md +99 -0
  20. data/docs/CONTRIBUTING.md +312 -0
  21. data/docs/IMPLEMENTATION_PLAN.md +1 -1
  22. data/docs/QUICK-START.md +0 -6
  23. data/docs/use_cases/UC-019-retention-based-routing.md +584 -0
  24. data/e11y.gemspec +28 -17
  25. data/lib/e11y/adapters/adaptive_batcher.rb +3 -0
  26. data/lib/e11y/adapters/audit_encrypted.rb +10 -4
  27. data/lib/e11y/adapters/base.rb +15 -0
  28. data/lib/e11y/adapters/file.rb +4 -1
  29. data/lib/e11y/adapters/in_memory.rb +6 -0
  30. data/lib/e11y/adapters/loki.rb +9 -0
  31. data/lib/e11y/adapters/otel_logs.rb +11 -9
  32. data/lib/e11y/adapters/sentry.rb +9 -0
  33. data/lib/e11y/adapters/yabeda.rb +54 -10
  34. data/lib/e11y/buffers.rb +8 -8
  35. data/lib/e11y/console.rb +52 -60
  36. data/lib/e11y/event/base.rb +75 -10
  37. data/lib/e11y/event/value_sampling_config.rb +10 -4
  38. data/lib/e11y/events/rails/http/request.rb +1 -1
  39. data/lib/e11y/instruments/active_job.rb +6 -3
  40. data/lib/e11y/instruments/rails_instrumentation.rb +51 -28
  41. data/lib/e11y/instruments/sidekiq.rb +7 -7
  42. data/lib/e11y/logger/bridge.rb +24 -54
  43. data/lib/e11y/metrics/cardinality_protection.rb +257 -12
  44. data/lib/e11y/metrics/cardinality_tracker.rb +17 -0
  45. data/lib/e11y/metrics/registry.rb +6 -2
  46. data/lib/e11y/metrics/relabeling.rb +0 -56
  47. data/lib/e11y/metrics.rb +6 -1
  48. data/lib/e11y/middleware/audit_signing.rb +12 -9
  49. data/lib/e11y/middleware/pii_filter.rb +18 -10
  50. data/lib/e11y/middleware/request.rb +10 -4
  51. data/lib/e11y/middleware/routing.rb +117 -90
  52. data/lib/e11y/middleware/sampling.rb +47 -28
  53. data/lib/e11y/middleware/trace_context.rb +40 -11
  54. data/lib/e11y/middleware/validation.rb +20 -2
  55. data/lib/e11y/middleware/versioning.rb +1 -1
  56. data/lib/e11y/pii.rb +7 -7
  57. data/lib/e11y/railtie.rb +24 -20
  58. data/lib/e11y/reliability/circuit_breaker.rb +3 -0
  59. data/lib/e11y/reliability/dlq/file_storage.rb +16 -5
  60. data/lib/e11y/reliability/dlq/filter.rb +3 -0
  61. data/lib/e11y/reliability/retry_handler.rb +4 -0
  62. data/lib/e11y/sampling/error_spike_detector.rb +16 -5
  63. data/lib/e11y/sampling/load_monitor.rb +13 -4
  64. data/lib/e11y/self_monitoring/reliability_monitor.rb +3 -0
  65. data/lib/e11y/version.rb +1 -1
  66. data/lib/e11y.rb +86 -9
  67. metadata +83 -38
  68. data/docs/use_cases/UC-019-tiered-storage-migration.md +0 -562
  69. data/lib/e11y/middleware/pii_filtering.rb +0 -280
  70. data/lib/e11y/middleware/slo.rb +0 -168
data/RELEASE.md ADDED
@@ -0,0 +1,254 @@
1
+ # Release Instructions for e11y
2
+
3
+ ## Quick Release (Automated)
4
+
5
+ ```bash
6
+ # 1. Bump version (updates VERSION + CHANGELOG)
7
+ rake release:bump
8
+ # Enter new version when prompted, e.g., 0.2.0
9
+
10
+ # 2. Review and commit
11
+ git diff
12
+ git add -A
13
+ git commit -m "Bump version to 0.2.0"
14
+
15
+ # 3. Full release (test + build + tag + push + publish)
16
+ rake release:full
17
+ ```
18
+
19
+ For more control, see [Step-by-Step Release](#step-by-step-release) below.
20
+
21
+ ## Pre-Release Checklist
22
+
23
+ - [ ] All changes documented in CHANGELOG.md under `[Unreleased]`
24
+ - [ ] Version bumped: `rake release:bump`
25
+ - [ ] All tests passing: `rake spec`
26
+ - [ ] Changes committed
27
+ - [ ] Git tag created
28
+ - [ ] Published to RubyGems.org
29
+ - [ ] GitHub release created
30
+
31
+ ## Step-by-Step Release
32
+
33
+ ### Step 0: Bump Version
34
+
35
+ First, update version and CHANGELOG:
36
+
37
+ ```bash
38
+ rake release:bump
39
+ ```
40
+
41
+ This will:
42
+ 1. Prompt for new version (e.g., 0.2.0)
43
+ 2. Update `lib/e11y/version.rb`
44
+ 3. Convert `[Unreleased]` → `[0.2.0] - YYYY-MM-DD` in CHANGELOG
45
+ 4. Add new empty `[Unreleased]` section
46
+
47
+ Commit the changes:
48
+
49
+ ```bash
50
+ git add -A
51
+ git commit -m "Bump version to 0.2.0"
52
+ ```
53
+
54
+ ### Step 1: Prepare Release
55
+
56
+ Run tests, build gem, create tag:
57
+
58
+ ```bash
59
+ rake release:prep
60
+ ```
61
+
62
+ This will:
63
+ - ✅ Check git status (fails if uncommitted changes)
64
+ - ✅ Run full test suite
65
+ - ✅ Build gem file
66
+ - ✅ Create annotated git tag
67
+
68
+ Or manually:
69
+
70
+ ```bash
71
+ # Run all tests
72
+ bundle exec rspec
73
+
74
+ # Build gem
75
+ gem build e11y.gemspec
76
+
77
+ # Create and push tag
78
+ git tag -a v0.2.0 -m "Release v0.2.0"
79
+ git push origin main
80
+ git push origin v0.2.0
81
+ ```
82
+
83
+ ### Step 2: Push to GitHub
84
+
85
+ ```bash
86
+ rake release:git_push
87
+ ```
88
+
89
+ This will:
90
+ - ✅ Verify tag exists
91
+ - ✅ Push commits to origin/main
92
+ - ✅ Push tag to origin
93
+ - ✅ Show GitHub release URL
94
+
95
+ ### Step 3: Publish to RubyGems.org
96
+
97
+ ```bash
98
+ rake release:gem_push
99
+ ```
100
+
101
+ This will:
102
+ - ✅ Verify gem file exists
103
+ - ✅ Prompt for confirmation
104
+ - ✅ Push gem to RubyGems (requires MFA)
105
+ - ✅ Show verification URL
106
+
107
+ Or manually:
108
+
109
+ ### Prerequisites
110
+
111
+ 1. **RubyGems Account**: Create account at https://rubygems.org/sign_up
112
+ 2. **API Key**: Get your API key from https://rubygems.org/profile/edit
113
+ 3. **MFA Enabled**: This gem requires MFA (configured in gemspec)
114
+
115
+ ### Publish Command
116
+
117
+ ```bash
118
+ # Sign in to RubyGems (one-time setup)
119
+ gem signin
120
+
121
+ # Push the gem (requires MFA)
122
+ gem push e11y-0.1.0.gem
123
+ ```
124
+
125
+ Expected output:
126
+ ```
127
+ Pushing gem to https://rubygems.org...
128
+ Enter your RubyGems.org credentials.
129
+ Username: [your_username]
130
+ Password: [your_password]
131
+ MFA Code: [6-digit code from authenticator app]
132
+ Successfully registered gem: e11y (0.1.0)
133
+ ```
134
+
135
+ ### Verify Publication
136
+
137
+ ```bash
138
+ # Check gem is available
139
+ gem search e11y --remote
140
+
141
+ # Install from RubyGems
142
+ gem install e11y
143
+ ```
144
+
145
+ ## Step 3: Create GitHub Release
146
+
147
+ 1. Go to https://github.com/arturseletskiy/e11y/releases/new
148
+ 2. Choose tag: `v0.1.0`
149
+ 3. Release title: `v0.1.0 - First Production Release`
150
+ 4. Description: Use content from CHANGELOG.md (see below)
151
+ 5. Attach binary: Upload `e11y-0.1.0.gem`
152
+ 6. Click "Publish release"
153
+
154
+ ### GitHub Release Notes Template
155
+
156
+ ```markdown
157
+ # 🎉 E11y 0.1.0 - First Production Release
158
+
159
+ Production-ready observability gem for Ruby on Rails with zero-config SLO tracking, request-scoped buffering, and high-performance event streaming.
160
+
161
+ ## 🚀 Highlights
162
+
163
+ - **Zero-Config SLO Tracking** - Automatic Service Level Objectives for HTTP and background jobs
164
+ - **100K+ events/sec** - Benchmark-validated performance (p99 <50μs latency)
165
+ - **99%+ Test Coverage** - 1409 test examples, battle-tested
166
+ - **16 ADRs** - Fully documented architecture decisions
167
+ - **Cardinality Protection** - 4-layer defense against metric explosions
168
+ - **Production-Ready** - Reliability layer with retry, circuit breaker, DLQ
169
+
170
+ ## 📦 Installation
171
+
172
+ ```ruby
173
+ # Gemfile
174
+ gem 'e11y', '~> 1.0'
175
+ ```
176
+
177
+ ```bash
178
+ bundle install
179
+ ```
180
+
181
+ ## 📚 Documentation
182
+
183
+ - **Quick Start**: [README.md](https://github.com/arturseletskiy/e11y#quick-start)
184
+ - **Architecture**: [docs/ADR-INDEX.md](https://github.com/arturseletskiy/e11y/blob/main/docs/ADR-INDEX.md)
185
+ - **Benchmarks**: [benchmarks/README.md](https://github.com/arturseletskiy/e11y/blob/main/benchmarks/README.md)
186
+
187
+ ## 🔥 What's New
188
+
189
+ See [CHANGELOG.md](https://github.com/arturseletskiy/e11y/blob/main/CHANGELOG.md) for full details.
190
+
191
+ ### Core Features (Phase 1-2)
192
+ - Event System with dry-schema validation
193
+ - Pipeline Architecture (middleware-based)
194
+ - 7 Adapters: Stdout, File, InMemory, Loki, Sentry, OpenTelemetry, Yabeda
195
+ - 3 Buffer Types: RingBuffer, RequestScopedBuffer, AdaptiveBuffer
196
+ - Reliability: Retry, Circuit Breaker, Dead Letter Queue
197
+
198
+ ### SLO Tracking (Phase 3)
199
+ - Event-Driven SLOs for HTTP/jobs
200
+ - Stratified Sampling (latency-aware)
201
+ - Error Spike Detection
202
+ - Value-Based Sampling
203
+
204
+ ### Rails Integration (Phase 4)
205
+ - Auto-instrumentation for Rails events
206
+ - HTTP request tracking
207
+ - Background job tracking (ActiveJob, Sidekiq)
208
+ - Database query events
209
+ - Logger bridge (Rails.logger → E11y)
210
+
211
+ ### Scale & Performance (Phase 5)
212
+ - Cardinality Protection (4-layer defense)
213
+ - Tiered Storage (hot/warm/cold)
214
+ - Benchmarked: 100K events/sec, p99 <50μs
215
+
216
+ ## ⚡ Performance
217
+
218
+ | Scale | Latency (p99) | Throughput | Memory |
219
+ |-------|--------------|------------|---------|
220
+ | Small (1K/s) | 47μs | 107K/s | 1.95 MB |
221
+ | Medium (10K/s) | 33μs | 110K/s | 19.49 MB |
222
+ | Large (100K/s) | 26μs | 109K/s | 194.93 MB |
223
+
224
+ ## 🙏 Credits
225
+
226
+ Built with patterns from Devise, Sidekiq, Puma, Dry-rb, Yabeda, and Sentry.
227
+
228
+ ---
229
+
230
+ **Full Changelog**: https://github.com/arturseletskiy/e11y/blob/main/CHANGELOG.md
231
+ ```
232
+
233
+ ## Post-Release Tasks
234
+
235
+ - [ ] Announce on Twitter/social media
236
+ - [ ] Update README badge with latest version
237
+ - [ ] Monitor RubyGems download stats
238
+ - [ ] Monitor GitHub issues for bug reports
239
+
240
+ ## Rollback Procedure (if needed)
241
+
242
+ If critical bug found after release:
243
+
244
+ ```bash
245
+ # Yank the gem (makes it unavailable for new installs)
246
+ gem yank e11y -v 0.1.0
247
+
248
+ # Fix the issue, bump to 1.0.1, and re-release
249
+ ```
250
+
251
+ ## Support
252
+
253
+ - **Issues**: https://github.com/arturseletskiy/e11y/issues
254
+ - **Discussions**: https://github.com/arturseletskiy/e11y/discussions
data/Rakefile CHANGED
@@ -1,5 +1,21 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ #
4
+ # E11y Gem Rakefile
5
+ #
6
+ # Quick reference:
7
+ # rake # Run tests and rubocop
8
+ # rake release:bump # Bump version and update CHANGELOG (interactive)
9
+ # rake release:full # Complete release workflow (prep + git_push + gem_push)
10
+ # rake release:prep # Run tests, build gem, create tag
11
+ # rake release:git_push # Push to GitHub
12
+ # rake release:gem_push # Publish to RubyGems
13
+ # rake spec:all # Run all test suites
14
+ # rake spec:unit # Run unit tests only (fast)
15
+ # rake spec:integration # Run integration tests
16
+ #
17
+ # See RELEASE.md for detailed release instructions
18
+
3
19
  require "bundler/gem_tasks"
4
20
  require "rspec/core/rake_task"
5
21
  require "rubocop/rake_task"
@@ -10,6 +26,82 @@ RuboCop::RakeTask.new
10
26
 
11
27
  task default: %i[spec rubocop]
12
28
 
29
+ # Test suite tasks
30
+ namespace :spec do
31
+ desc "Run unit tests only (fast, no Rails/integrations)"
32
+ task :unit do
33
+ sh "bundle exec rspec spec/e11y spec/e11y_spec.rb spec/zeitwerk_spec.rb"
34
+ end
35
+
36
+ desc "Run integration tests (requires Rails, bundle install --with integration)"
37
+ task :integration do
38
+ # Run integration tests with explicit file patterns to avoid loading all specs
39
+ # This prevents test pollution from unit test files
40
+ sh "INTEGRATION=true bundle exec rspec " \
41
+ "spec/integration/*.rb " \
42
+ "spec/e11y/adapters/*_spec.rb " \
43
+ "spec/e11y/instruments/*_spec.rb " \
44
+ "--tag integration"
45
+ end
46
+
47
+ desc "Run railtie integration tests (separate Rails app instance)"
48
+ task :railtie do
49
+ sh "bundle exec rspec spec/e11y/railtie_integration_spec.rb --tag railtie_integration"
50
+ end
51
+
52
+ desc "Run all tests (unit + integration + railtie, ~1729 examples)"
53
+ task :all do
54
+ puts "\n#{'=' * 80}"
55
+ puts "Running UNIT tests (spec/e11y + top-level specs)..."
56
+ puts "#{'=' * 80}\n"
57
+ Rake::Task["spec:unit"].invoke
58
+
59
+ puts "\n#{'=' * 80}"
60
+ puts "Running INTEGRATION tests (spec/integration)..."
61
+ puts "#{'=' * 80}\n"
62
+ Rake::Task["spec:integration"].invoke
63
+
64
+ puts "\n#{'=' * 80}"
65
+ puts "Running RAILTIE tests (Rails initialization)..."
66
+ puts "#{'=' * 80}\n"
67
+ Rake::Task["spec:railtie"].invoke
68
+
69
+ puts "\n#{'=' * 80}"
70
+ puts "✅ All test suites completed!"
71
+ puts "#{'=' * 80}\n"
72
+ end
73
+
74
+ desc "Run tests with coverage report"
75
+ task :coverage do
76
+ sh "COVERAGE=true bundle exec rspec"
77
+ end
78
+
79
+ desc "Run integration tests with coverage"
80
+ task :coverage_integration do
81
+ sh "COVERAGE=true INTEGRATION=true bundle exec rspec spec/integration/"
82
+ end
83
+
84
+ desc "Run benchmark tests (performance tests, slow)"
85
+ task :benchmark do
86
+ sh "bundle exec rspec spec/e11y --tag benchmark"
87
+ end
88
+
89
+ desc "Run ALL tests including benchmarks (very slow)"
90
+ task :everything do
91
+ puts "\n#{'=' * 80}"
92
+ puts "Running ALL tests (unit + integration + railtie + benchmarks)"
93
+ puts "#{'=' * 80}\n"
94
+ Rake::Task["spec:unit"].invoke
95
+ Rake::Task["spec:integration"].invoke
96
+ Rake::Task["spec:railtie"].invoke
97
+ Rake::Task["spec:benchmark"].invoke
98
+
99
+ puts "\n#{'=' * 80}"
100
+ puts "✅ All test suites including benchmarks completed!"
101
+ puts "#{'=' * 80}\n"
102
+ end
103
+ end
104
+
13
105
  # Custom tasks
14
106
  namespace :e11y do
15
107
  desc "Start interactive console"
@@ -35,3 +127,288 @@ namespace :e11y do
35
127
  sh "bundle exec brakeman --no-pager"
36
128
  end
37
129
  end
130
+
131
+ # Custom release automation (extends bundler/gem_tasks)
132
+ # Note: bundler/gem_tasks provides: release, release:guard_clean, release:rubygem_push, etc.
133
+ # Our tasks provide more control and visibility
134
+ namespace :release do
135
+ desc "Bump version and update CHANGELOG (interactive)"
136
+ task :bump do
137
+ require_relative "lib/e11y/version"
138
+ current_version = E11y::VERSION
139
+
140
+ puts "\n#{'=' * 80}"
141
+ puts "📝 Version Bump"
142
+ puts "#{'=' * 80}\n"
143
+ puts "Current version: #{current_version}"
144
+ puts "\nEnter new version (e.g., 0.2.0, 1.0.0):"
145
+
146
+ new_version = $stdin.gets.chomp.strip
147
+
148
+ if new_version.empty?
149
+ puts "❌ Error: Version cannot be empty"
150
+ exit 1
151
+ end
152
+
153
+ unless new_version.match?(/^\d+\.\d+\.\d+$/)
154
+ puts "❌ Error: Invalid version format. Use semantic versioning (e.g., 0.2.0)"
155
+ exit 1
156
+ end
157
+
158
+ if new_version == current_version
159
+ puts "⚠️ Warning: New version is the same as current version"
160
+ puts "Continue anyway? (y/N)"
161
+ response = $stdin.gets.chomp.downcase
162
+ exit 0 unless response == "y" || response == "yes"
163
+ end
164
+
165
+ puts "\n[1/3] Updating lib/e11y/version.rb..."
166
+ version_file = "lib/e11y/version.rb"
167
+ version_content = File.read(version_file)
168
+ updated_version_content = version_content.gsub(
169
+ /VERSION = "#{Regexp.escape(current_version)}"/,
170
+ "VERSION = \"#{new_version}\""
171
+ )
172
+ File.write(version_file, updated_version_content)
173
+ puts "✅ Updated: #{current_version} → #{new_version}"
174
+
175
+ puts "\n[2/3] Updating CHANGELOG.md..."
176
+ changelog_file = "CHANGELOG.md"
177
+ changelog_content = File.read(changelog_file)
178
+
179
+ # Check if there's an [Unreleased] section
180
+ if changelog_content.include?("## [Unreleased]")
181
+ # Replace [Unreleased] with version and date
182
+ today = Time.now.strftime("%Y-%m-%d")
183
+ updated_changelog = changelog_content.sub(
184
+ /## \[Unreleased\]/,
185
+ "## [#{new_version}] - #{today}"
186
+ )
187
+
188
+ # Add new [Unreleased] section at the top
189
+ updated_changelog = updated_changelog.sub(
190
+ /(## \[#{Regexp.escape(new_version)}\] - #{today})/,
191
+ "## [Unreleased]\n\n### Added\n\n### Changed\n\n### Fixed\n\n### Deprecated\n\n### Removed\n\n### Security\n\n\\1"
192
+ )
193
+
194
+ File.write(changelog_file, updated_changelog)
195
+ puts "✅ Updated CHANGELOG.md:"
196
+ puts " - [Unreleased] → [#{new_version}] - #{today}"
197
+ puts " - Added new [Unreleased] section"
198
+ else
199
+ # No [Unreleased] section, just add version entry
200
+ today = Time.now.strftime("%Y-%m-%d")
201
+
202
+ # Find where to insert (after the header, before first version)
203
+ if changelog_content =~ /(## \[\d+\.\d+\.\d+\])/
204
+ updated_changelog = changelog_content.sub(
205
+ /(## \[\d+\.\d+\.\d+\])/,
206
+ "## [Unreleased]\n\n### Added\n\n### Changed\n\n### Fixed\n\n### Deprecated\n\n### Removed\n\n### Security\n\n## [#{new_version}] - #{today}\n\n### Added\n- Version bump\n\n\\1"
207
+ )
208
+ else
209
+ # No previous versions, add after header
210
+ header_end = changelog_content.index("\n\n") || 0
211
+ header = changelog_content[0..header_end]
212
+ rest = changelog_content[header_end + 1..-1] || ""
213
+ updated_changelog = "#{header}\n## [#{new_version}] - #{today}\n\n### Added\n- Initial release\n\n#{rest}"
214
+ end
215
+
216
+ File.write(changelog_file, updated_changelog)
217
+ puts "✅ Added version [#{new_version}] - #{today} to CHANGELOG.md"
218
+ end
219
+
220
+ puts "\n[3/3] Summary"
221
+ puts "✅ Version bumped: #{current_version} → #{new_version}"
222
+ puts "✅ Files updated:"
223
+ puts " - lib/e11y/version.rb"
224
+ puts " - CHANGELOG.md"
225
+
226
+ puts "\n#{'=' * 80}"
227
+ puts "Next steps:"
228
+ puts " 1. Review changes: git diff"
229
+ puts " 2. Commit changes: git add -A && git commit -m 'Bump version to #{new_version}'"
230
+ puts " 3. Release: rake release:prep"
231
+ puts "#{'=' * 80}\n"
232
+ end
233
+
234
+ desc "Prepare release: run tests, build gem, create git tag (safe)"
235
+ task :prep do
236
+ require_relative "lib/e11y/version"
237
+ version = E11y::VERSION
238
+
239
+ puts "\n#{'=' * 80}"
240
+ puts "📦 Preparing release for e11y v#{version}"
241
+ puts "#{'=' * 80}\n"
242
+
243
+ # Step 1: Check git status
244
+ puts "\n[1/5] Checking git status..."
245
+ unless system("git diff-index --quiet HEAD --")
246
+ puts "❌ Error: You have uncommitted changes. Please commit them first."
247
+ exit 1
248
+ end
249
+ puts "✅ Git working directory is clean"
250
+
251
+ # Step 2: Run tests
252
+ puts "\n[2/5] Running tests..."
253
+ unless system("bundle exec rspec")
254
+ puts "❌ Error: Tests failed. Please fix them before releasing."
255
+ exit 1
256
+ end
257
+ puts "✅ All tests passed"
258
+
259
+ # Step 3: Build gem
260
+ puts "\n[3/5] Building gem..."
261
+ unless system("gem build e11y.gemspec")
262
+ puts "❌ Error: Failed to build gem"
263
+ exit 1
264
+ end
265
+ puts "✅ Gem built: e11y-#{version}.gem"
266
+
267
+ # Step 4: Create git tag
268
+ puts "\n[4/5] Creating git tag..."
269
+ tag_name = "v#{version}"
270
+ tag_message = "Release v#{version}"
271
+
272
+ if system("git rev-parse #{tag_name} >/dev/null 2>&1")
273
+ puts "⚠️ Warning: Tag #{tag_name} already exists"
274
+ else
275
+ unless system("git tag -a #{tag_name} -m '#{tag_message}'")
276
+ puts "❌ Error: Failed to create git tag"
277
+ exit 1
278
+ end
279
+ puts "✅ Git tag created: #{tag_name}"
280
+ end
281
+
282
+ # Step 5: Summary
283
+ puts "\n[5/5] Release preparation complete!"
284
+ puts "\n#{'=' * 80}"
285
+ puts "📦 Release v#{version} is ready!"
286
+ puts "#{'=' * 80}\n"
287
+ puts "Next steps:"
288
+ puts " 1. Review CHANGELOG.md"
289
+ puts " 2. Push to GitHub:"
290
+ puts " git push origin main"
291
+ puts " git push origin #{tag_name}"
292
+ puts " 3. Publish to RubyGems:"
293
+ puts " rake release:publish"
294
+ puts "\n"
295
+ end
296
+
297
+ desc "Publish gem to RubyGems.org (requires authentication, safe)"
298
+ task :gem_push do
299
+ require_relative "lib/e11y/version"
300
+ version = E11y::VERSION
301
+ gem_file = "e11y-#{version}.gem"
302
+
303
+ puts "\n#{'=' * 80}"
304
+ puts "📤 Publishing e11y v#{version} to RubyGems.org"
305
+ puts "#{'=' * 80}\n"
306
+
307
+ unless File.exist?(gem_file)
308
+ puts "❌ Error: Gem file not found: #{gem_file}"
309
+ puts "Run 'rake release:prep' first"
310
+ exit 1
311
+ end
312
+
313
+ puts "This will publish #{gem_file} to RubyGems.org"
314
+ puts "You will be prompted for your RubyGems credentials and MFA code."
315
+ puts "\nContinue? (y/N)"
316
+
317
+ response = $stdin.gets.chomp.downcase
318
+ unless %w[y yes].include?(response)
319
+ puts "❌ Publication cancelled"
320
+ exit 0
321
+ end
322
+
323
+ unless system("gem push #{gem_file}")
324
+ puts "\n❌ Error: Failed to publish gem"
325
+ puts "Make sure you have:"
326
+ puts " 1. RubyGems account (https://rubygems.org/sign_up)"
327
+ puts " 2. Signed in: gem signin"
328
+ puts " 3. MFA enabled on your account"
329
+ exit 1
330
+ end
331
+
332
+ puts "\n✅ Successfully published e11y v#{version} to RubyGems.org!"
333
+ puts "\nVerify: https://rubygems.org/gems/e11y/versions/#{version}"
334
+ end
335
+
336
+ desc "Push git changes and tag to GitHub (safe)"
337
+ task :git_push do
338
+ require_relative "lib/e11y/version"
339
+ version = E11y::VERSION
340
+ tag_name = "v#{version}"
341
+
342
+ puts "\n#{'=' * 80}"
343
+ puts "🚀 Pushing to GitHub"
344
+ puts "#{'=' * 80}\n"
345
+
346
+ unless system("git rev-parse #{tag_name} >/dev/null 2>&1")
347
+ puts "❌ Error: Tag #{tag_name} does not exist"
348
+ puts "Run 'rake release:prep' first"
349
+ exit 1
350
+ end
351
+
352
+ puts "[1/2] Pushing commits to origin/main..."
353
+ unless system("git push origin main")
354
+ puts "❌ Error: Failed to push commits"
355
+ exit 1
356
+ end
357
+ puts "✅ Commits pushed"
358
+
359
+ puts "\n[2/2] Pushing tag #{tag_name}..."
360
+ unless system("git push origin #{tag_name}")
361
+ puts "❌ Error: Failed to push tag"
362
+ exit 1
363
+ end
364
+ puts "✅ Tag pushed"
365
+
366
+ puts "\n✅ Successfully pushed to GitHub!"
367
+ puts "\nCreate GitHub release: https://github.com/arturseletskiy/e11y/releases/new?tag=#{tag_name}"
368
+ end
369
+
370
+ desc "Complete release workflow: prep, git_push, and gem_push (interactive)"
371
+ task :full do
372
+ Rake::Task["release:prep"].invoke
373
+
374
+ puts "\n#{'=' * 80}"
375
+ puts "Ready to push to GitHub and publish to RubyGems?"
376
+ puts "=" * 80
377
+ puts "This will:"
378
+ puts " 1. Push commits and tag to GitHub"
379
+ puts " 2. Publish gem to RubyGems.org"
380
+ puts "\nContinue? (y/N)"
381
+
382
+ response = $stdin.gets.chomp.downcase
383
+ unless %w[y yes].include?(response)
384
+ puts "\n⏸️ Release prepared but not published"
385
+ puts "To continue later, run:"
386
+ puts " rake release:git_push # Push to GitHub"
387
+ puts " rake release:gem_push # Publish to RubyGems"
388
+ exit 0
389
+ end
390
+
391
+ Rake::Task["release:git_push"].invoke
392
+ Rake::Task["release:gem_push"].invoke
393
+
394
+ puts "\n#{'=' * 80}"
395
+ puts "🎉 Release complete!"
396
+ puts "=" * 80
397
+ puts "\nPost-release tasks:"
398
+ puts " 1. Create GitHub release: https://github.com/arturseletskiy/e11y/releases/new"
399
+ puts " 2. Verify on RubyGems: https://rubygems.org/gems/e11y"
400
+ puts " 3. Update README badges"
401
+ puts " 4. Announce on social media"
402
+ puts "\n"
403
+ end
404
+
405
+ desc "Clean up built gems"
406
+ task :clean do
407
+ puts "🧹 Cleaning up gem files..."
408
+ FileList["*.gem"].each do |gem_file|
409
+ File.delete(gem_file)
410
+ puts " Deleted: #{gem_file}"
411
+ end
412
+ puts "✅ Clean complete"
413
+ end
414
+ end