bug_bunny 4.8.1 → 4.9.1

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.
@@ -15,6 +15,7 @@ RSpec.describe BugBunny::Consumer do
15
15
  allow(session).to receive(:exchange).and_return(double('exchange'))
16
16
  allow(session).to receive(:queue).and_return(fake_queue)
17
17
  allow(session).to receive(:close)
18
+ allow(session).to receive(:channel).and_return(channel)
18
19
  session
19
20
  end
20
21
 
@@ -29,6 +30,137 @@ RSpec.describe BugBunny::Consumer do
29
30
  consumer.instance_variable_set(:@session, fake_session)
30
31
  end
31
32
 
33
+ describe 'OTel messaging semantic conventions en logs' do
34
+ let(:mock_channel) do
35
+ ch = double('channel')
36
+ allow(ch).to receive(:reject)
37
+ allow(ch).to receive(:ack)
38
+ allow(ch).to receive(:open?).and_return(true)
39
+ default_ex = double('default_exchange')
40
+ allow(default_ex).to receive(:publish)
41
+ allow(ch).to receive(:default_exchange).and_return(default_ex)
42
+ ch
43
+ end
44
+
45
+ let(:mock_session) do
46
+ s = instance_double(BugBunny::Session)
47
+ allow(s).to receive(:exchange).and_return(double('exchange'))
48
+ allow(s).to receive(:queue).and_return(fake_queue)
49
+ allow(s).to receive(:close)
50
+ allow(s).to receive(:channel).and_return(mock_channel)
51
+ s
52
+ end
53
+
54
+ let(:otel_consumer) do
55
+ c = described_class.new(connection)
56
+ c.instance_variable_set(:@session, mock_session)
57
+ c
58
+ end
59
+
60
+ let(:delivery_info) do
61
+ double('delivery_info',
62
+ exchange: 'events_x',
63
+ routing_key: 'users.created',
64
+ delivery_tag: 'delivery-tag-1',
65
+ redelivered?: false)
66
+ end
67
+
68
+ let(:properties) do
69
+ double('properties',
70
+ type: 'users/show',
71
+ headers: { 'x-http-method' => 'GET' },
72
+ correlation_id: 'corr-abc-123',
73
+ reply_to: 'amq.rabbitmq.reply-to',
74
+ content_type: 'application/json')
75
+ end
76
+
77
+ let(:logged_events) { [] }
78
+
79
+ before do
80
+ allow(otel_consumer).to receive(:safe_log) do |level, event, **kwargs|
81
+ logged_events << { level: level, event: event, kwargs: kwargs }
82
+ end
83
+ allow(otel_consumer).to receive(:handle_fatal_error)
84
+ end
85
+
86
+ describe '#process_message — log events incluyen campos OTel' do
87
+ it 'consumer.message_received incluye otel_fields con operation=process' do
88
+ allow(BugBunny.routes).to receive(:recognize).and_return(
89
+ { controller: 'user', action: 'show', params: {}, namespace: nil }
90
+ )
91
+
92
+ controller_class = Class.new(BugBunny::Controller) do
93
+ def show
94
+ render status: 200, json: { ok: true }
95
+ end
96
+ end
97
+ stub_const('BugBunny::Controllers::UserController', controller_class)
98
+
99
+ otel_consumer.send(:process_message, delivery_info, properties, '{}')
100
+
101
+ received_event = logged_events.find { |e| e[:event] == 'consumer.message_received' }
102
+ expect(received_event).not_to be_nil
103
+ expect(received_event[:kwargs]).to include(
104
+ messaging_system: 'rabbitmq',
105
+ messaging_operation: 'process',
106
+ messaging_destination_name: 'events_x',
107
+ messaging_routing_key: 'users.created',
108
+ messaging_message_id: 'corr-abc-123'
109
+ )
110
+ end
111
+
112
+ it 'consumer.message_processed incluye otel_fields con operation=process' do
113
+ allow(BugBunny.routes).to receive(:recognize).and_return(
114
+ { controller: 'user', action: 'show', params: {}, namespace: nil }
115
+ )
116
+
117
+ controller_class = Class.new(BugBunny::Controller) do
118
+ def show
119
+ render status: 200, json: { ok: true }
120
+ end
121
+ end
122
+ stub_const('BugBunny::Controllers::UserController', controller_class)
123
+
124
+ otel_consumer.send(:process_message, delivery_info, properties, '{}')
125
+
126
+ processed_event = logged_events.find { |e| e[:event] == 'consumer.message_processed' }
127
+ expect(processed_event).not_to be_nil
128
+ expect(processed_event[:kwargs]).to include(
129
+ messaging_system: 'rabbitmq',
130
+ messaging_operation: 'process',
131
+ messaging_destination_name: 'events_x',
132
+ messaging_routing_key: 'users.created',
133
+ messaging_message_id: 'corr-abc-123'
134
+ )
135
+ end
136
+
137
+ it 'consumer.message_received omite messaging_message_id cuando no hay correlation_id' do
138
+ properties_no_corr = double('properties',
139
+ type: 'users/show',
140
+ headers: { 'x-http-method' => 'GET' },
141
+ correlation_id: nil,
142
+ reply_to: nil,
143
+ content_type: 'application/json')
144
+
145
+ allow(BugBunny.routes).to receive(:recognize).and_return(
146
+ { controller: 'user', action: 'show', params: {}, namespace: nil }
147
+ )
148
+
149
+ controller_class = Class.new(BugBunny::Controller) do
150
+ def show
151
+ render status: 200, json: { ok: true }
152
+ end
153
+ end
154
+ stub_const('BugBunny::Controllers::UserController', controller_class)
155
+
156
+ otel_consumer.send(:process_message, delivery_info, properties_no_corr, '{}')
157
+
158
+ received_event = logged_events.find { |e| e[:event] == 'consumer.message_received' }
159
+ expect(received_event[:kwargs]).not_to have_key(:messaging_message_id)
160
+ end
161
+ end
162
+ end
163
+
32
164
  describe '#shutdown' do
33
165
  it 'detiene el health timer' do
34
166
  timer = instance_double(Concurrent::TimerTask)
@@ -57,10 +189,10 @@ RSpec.describe BugBunny::Consumer do
57
189
  consumer.define_singleton_method(:shutdown) { shutdown_called = true }
58
190
 
59
191
  consumer.subscribe(
60
- queue_name: 'q',
192
+ queue_name: 'q',
61
193
  exchange_name: 'x',
62
- routing_key: '#',
63
- block: false
194
+ routing_key: '#',
195
+ block: false
64
196
  )
65
197
 
66
198
  expect(shutdown_called).to be(true)
@@ -75,10 +207,10 @@ RSpec.describe BugBunny::Consumer do
75
207
 
76
208
  expect do
77
209
  consumer.subscribe(
78
- queue_name: 'q',
210
+ queue_name: 'q',
79
211
  exchange_name: 'x',
80
- routing_key: '#',
81
- block: false
212
+ routing_key: '#',
213
+ block: false
82
214
  )
83
215
  end.to raise_error(RuntimeError)
84
216
 
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ RSpec.describe BugBunny::OTel do
6
+ describe '.messaging_headers' do
7
+ it 'emite los campos base con messaging.system = rabbitmq' do
8
+ headers = described_class.messaging_headers(
9
+ operation: 'publish',
10
+ destination: 'events_x',
11
+ routing_key: 'users.created'
12
+ )
13
+
14
+ expect(headers).to include(
15
+ :messaging_system => 'rabbitmq',
16
+ :messaging_operation => 'publish',
17
+ :messaging_destination_name => 'events_x',
18
+ :messaging_routing_key => 'users.created'
19
+ )
20
+ end
21
+
22
+ it 'omite messaging.message.id cuando message_id es nil' do
23
+ headers = described_class.messaging_headers(
24
+ operation: 'publish',
25
+ destination: 'x',
26
+ routing_key: 'rk'
27
+ )
28
+
29
+ expect(headers).not_to have_key(:messaging_message_id)
30
+ end
31
+
32
+ it 'incluye messaging.message.id cuando se provee' do
33
+ headers = described_class.messaging_headers(
34
+ operation: 'publish',
35
+ destination: 'x',
36
+ routing_key: 'rk',
37
+ message_id: 'abc-123'
38
+ )
39
+
40
+ expect(headers[:messaging_message_id]).to eq('abc-123')
41
+ end
42
+
43
+ it 'coacciona destination y routing_key nil a string vacío' do
44
+ headers = described_class.messaging_headers(
45
+ operation: 'receive',
46
+ destination: nil,
47
+ routing_key: nil
48
+ )
49
+
50
+ expect(headers[:messaging_destination_name]).to eq('')
51
+ expect(headers[:messaging_routing_key]).to eq('')
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,187 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+ require 'support/bunny_mocks'
5
+
6
+ RSpec.describe BugBunny::Producer do
7
+ include BunnyMocks
8
+
9
+ let(:channel) { BunnyMocks::FakeChannel.new(true) }
10
+ let(:connection) { BunnyMocks::FakeConnection.new(true, channel) }
11
+ let(:session) { BugBunny::Session.new(connection) }
12
+ let(:producer) { described_class.new(session) }
13
+
14
+ let(:logged_events) { [] }
15
+
16
+ before do
17
+ allow(producer).to receive(:safe_log) do |level, event, **kwargs|
18
+ logged_events << { level: level, event: event, kwargs: kwargs }
19
+ end
20
+ end
21
+
22
+ describe 'OTel messaging semantic conventions en logs' do
23
+ describe '#fire — log events incluyen campos OTel' do
24
+ it 'producer.publish incluye otel_fields con operation=publish' do
25
+ request = BugBunny::Request.new('users')
26
+ request.exchange = 'users_x'
27
+ request.method = :post
28
+ request.body = { name: 'test' }
29
+
30
+ fake_exchange = double('exchange')
31
+ allow(session).to receive(:exchange).and_return(fake_exchange)
32
+ allow(fake_exchange).to receive(:publish)
33
+
34
+ producer.fire(request)
35
+
36
+ publish_event = logged_events.find { |e| e[:event] == 'producer.publish' }
37
+ expect(publish_event).not_to be_nil
38
+ expect(publish_event[:kwargs]).to include(
39
+ method: 'POST',
40
+ path: 'users',
41
+ messaging_system: 'rabbitmq',
42
+ messaging_operation: 'publish',
43
+ messaging_destination_name: 'users_x',
44
+ messaging_routing_key: 'users'
45
+ )
46
+ end
47
+
48
+ it 'producer.publish incluye messaging_message_id cuando hay correlation_id' do
49
+ request = BugBunny::Request.new('users/42')
50
+ request.exchange = 'users_x'
51
+ request.correlation_id = 'corr-xyz-789'
52
+ request.method = :get
53
+
54
+ fake_exchange = double('exchange')
55
+ allow(session).to receive(:exchange).and_return(fake_exchange)
56
+ allow(fake_exchange).to receive(:publish)
57
+
58
+ producer.fire(request)
59
+
60
+ publish_event = logged_events.find { |e| e[:event] == 'producer.publish' }
61
+ expect(publish_event[:kwargs]).to include(
62
+ messaging_message_id: 'corr-xyz-789'
63
+ )
64
+ end
65
+
66
+ it 'producer.publish omite messaging_message_id cuando no hay correlation_id' do
67
+ request = BugBunny::Request.new('users')
68
+ request.exchange = 'users_x'
69
+ request.method = :post
70
+
71
+ fake_exchange = double('exchange')
72
+ allow(session).to receive(:exchange).and_return(fake_exchange)
73
+ allow(fake_exchange).to receive(:publish)
74
+
75
+ producer.fire(request)
76
+
77
+ publish_event = logged_events.find { |e| e[:event] == 'producer.publish' }
78
+ expect(publish_event[:kwargs]).not_to have_key(:messaging_message_id)
79
+ end
80
+
81
+ it 'producer.publish_detail incluye messaging_destination_name' do
82
+ request = BugBunny::Request.new('users')
83
+ request.exchange = 'events_x'
84
+ request.method = :post
85
+
86
+ fake_exchange = double('exchange')
87
+ allow(session).to receive(:exchange).and_return(fake_exchange)
88
+ allow(fake_exchange).to receive(:publish)
89
+
90
+ producer.fire(request)
91
+
92
+ detail_event = logged_events.find { |e| e[:event] == 'producer.publish_detail' }
93
+ expect(detail_event).not_to be_nil
94
+ expect(detail_event[:kwargs]).to include(
95
+ messaging_destination_name: 'events_x'
96
+ )
97
+ end
98
+ end
99
+
100
+ describe '#rpc — log events incluyen campos OTel' do
101
+ let(:fake_exchange) { double('exchange') }
102
+ let(:mock_channel) do
103
+ ch = double('channel')
104
+ allow(ch).to receive(:publish)
105
+ allow(ch).to receive(:basic_consume)
106
+ allow(ch).to receive(:open?).and_return(true)
107
+ ch
108
+ end
109
+
110
+ let(:mock_session) do
111
+ s = instance_double(BugBunny::Session)
112
+ allow(s).to receive(:exchange).and_return(fake_exchange)
113
+ allow(s).to receive(:channel).and_return(mock_channel)
114
+ s
115
+ end
116
+
117
+ let(:rpc_producer) { described_class.new(mock_session) }
118
+
119
+ before do
120
+ allow(rpc_producer).to receive(:safe_log) do |level, event, **kwargs|
121
+ logged_events << { level: level, event: event, kwargs: kwargs }
122
+ end
123
+ allow(fake_exchange).to receive(:publish)
124
+ end
125
+
126
+ it 'producer.rpc_waiting incluye messaging_message_id' do
127
+ request = BugBunny::Request.new('users')
128
+ request.exchange = 'users_x'
129
+ request.method = :get
130
+
131
+ allow(rpc_producer).to receive(:ensure_reply_listener!)
132
+
133
+ ivar = Concurrent::IVar.new
134
+ allow(Concurrent::IVar).to receive(:new).and_return(ivar)
135
+ allow(rpc_producer).to receive(:sleep)
136
+
137
+ request.correlation_id = 'test-cid'
138
+
139
+ rpc_producer.instance_variable_get(:@pending_requests)['test-cid'] = ivar
140
+
141
+ thread = Thread.new do
142
+ sleep 0.05
143
+ ivar.set({ body: '{"ok":true}', headers: {} })
144
+ end
145
+ thread.join
146
+
147
+ rpc_producer.rpc(request)
148
+
149
+ waiting_event = logged_events.find { |e| e[:event] == 'producer.rpc_waiting' }
150
+ expect(waiting_event).not_to be_nil
151
+ expect(waiting_event[:kwargs]).to include(
152
+ messaging_message_id: 'test-cid',
153
+ timeout_s: an_instance_of(Integer)
154
+ )
155
+ end
156
+
157
+ it 'producer.rpc_response_received incluye otel_fields con operation=receive' do
158
+ request = BugBunny::Request.new('users')
159
+ request.exchange = 'users_x'
160
+ request.method = :get
161
+
162
+ allow(rpc_producer).to receive(:ensure_reply_listener!)
163
+
164
+ ivar = Concurrent::IVar.new
165
+ allow(Concurrent::IVar).to receive(:new).and_return(ivar)
166
+ allow(rpc_producer).to receive(:sleep)
167
+
168
+ request.correlation_id = 'corr-reply-test'
169
+
170
+ rpc_producer.instance_variable_get(:@pending_requests)['corr-reply-test'] = ivar
171
+
172
+ thread = Thread.new { ivar.set({ body: '{"ok":true}', headers: {} }) }
173
+ thread.join(0.1)
174
+
175
+ rpc_producer.rpc(request)
176
+
177
+ response_event = logged_events.find { |e| e[:event] == 'producer.rpc_response_received' }
178
+ expect(response_event).not_to be_nil
179
+ expect(response_event[:kwargs]).to include(
180
+ messaging_system: 'rabbitmq',
181
+ messaging_operation: 'receive',
182
+ messaging_message_id: 'corr-reply-test'
183
+ )
184
+ end
185
+ end
186
+ end
187
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ RSpec.describe BugBunny::Request do
6
+ subject(:request) do
7
+ req = described_class.new('users/42')
8
+ req.exchange = 'users_x'
9
+ req.method = :post
10
+ req
11
+ end
12
+
13
+ describe '#amqp_options' do
14
+ it 'inyecta los campos OTel con operation=publish como string keys' do
15
+ headers = request.amqp_options[:headers]
16
+
17
+ expect(headers).to include(
18
+ 'messaging_system' => 'rabbitmq',
19
+ 'messaging_operation' => 'publish',
20
+ 'messaging_destination_name' => 'users_x',
21
+ 'messaging_routing_key' => 'users/42'
22
+ )
23
+ end
24
+
25
+ it 'incluye messaging_message_id cuando hay correlation_id' do
26
+ request.correlation_id = 'corr-xyz'
27
+
28
+ expect(request.amqp_options[:headers]['messaging_message_id']).to eq('corr-xyz')
29
+ end
30
+
31
+ it 'omite messaging_message_id cuando no hay correlation_id' do
32
+ expect(request.amqp_options[:headers]).not_to have_key('messaging_message_id')
33
+ end
34
+
35
+ it 'preserva x-http-method con el verbo en mayúsculas' do
36
+ expect(request.amqp_options[:headers]['x-http-method']).to eq('POST')
37
+ end
38
+
39
+ it 'permite a headers del usuario sobrescribir campos OTel' do
40
+ request.headers = { 'messaging_system' => 'custom-broker' }
41
+
42
+ expect(request.amqp_options[:headers]['messaging_system']).to eq('custom-broker')
43
+ end
44
+
45
+ it 'no permite al usuario pisar x-http-method' do
46
+ request.headers = { 'x-http-method' => 'HACK' }
47
+
48
+ expect(request.amqp_options[:headers]['x-http-method']).to eq('POST')
49
+ end
50
+ end
51
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: bug_bunny
3
3
  version: !ruby/object:Gem::Version
4
- version: 4.8.1
4
+ version: 4.9.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - gabix
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-04-04 00:00:00.000000000 Z
11
+ date: 2026-04-07 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bunny
@@ -229,18 +229,6 @@ executables: []
229
229
  extensions: []
230
230
  extra_rdoc_files: []
231
231
  files:
232
- - ".agents/skills/documentation-writer/SKILL.md"
233
- - ".agents/skills/gem-release/SKILL.md"
234
- - ".agents/skills/quality-code/SKILL.md"
235
- - ".agents/skills/rabbitmq-expert/SKILL.md"
236
- - ".agents/skills/sentry/SKILL.md"
237
- - ".agents/skills/sentry/references/api-endpoints.md"
238
- - ".agents/skills/sentry/scripts/sentry.rb"
239
- - ".agents/skills/skill-builder/SKILL.md"
240
- - ".agents/skills/skill-manager/SKILL.md"
241
- - ".agents/skills/skill-manager/scripts/sync.rb"
242
- - ".agents/skills/yard/SKILL.md"
243
- - ".agents/skills/yard/references/tipos.md"
244
232
  - CHANGELOG.md
245
233
  - CLAUDE.md
246
234
  - README.md
@@ -258,6 +246,7 @@ files:
258
246
  - lib/bug_bunny/middleware/raise_error.rb
259
247
  - lib/bug_bunny/middleware/stack.rb
260
248
  - lib/bug_bunny/observability.rb
249
+ - lib/bug_bunny/otel.rb
261
250
  - lib/bug_bunny/producer.rb
262
251
  - lib/bug_bunny/railtie.rb
263
252
  - lib/bug_bunny/request.rb
@@ -296,6 +285,9 @@ files:
296
285
  - spec/unit/consumer_spec.rb
297
286
  - spec/unit/controller_after_action_spec.rb
298
287
  - spec/unit/observability_spec.rb
288
+ - spec/unit/otel_spec.rb
289
+ - spec/unit/producer_spec.rb
290
+ - spec/unit/request_spec.rb
299
291
  - spec/unit/resource_attributes_spec.rb
300
292
  - spec/unit/session_spec.rb
301
293
  - test/integration/infrastructure_test.rb
@@ -308,7 +300,7 @@ metadata:
308
300
  homepage_uri: https://github.com/gedera/bug_bunny
309
301
  source_code_uri: https://github.com/gedera/bug_bunny
310
302
  changelog_uri: https://github.com/gedera/bug_bunny/blob/main/CHANGELOG.md
311
- documentation_uri: https://github.com/gedera/bug_bunny/blob/v4.8.1/skill
303
+ documentation_uri: https://github.com/gedera/bug_bunny/blob/v4.9.1/skill
312
304
  post_install_message:
313
305
  rdoc_options: []
314
306
  require_paths:
@@ -1,45 +0,0 @@
1
- ---
2
- name: documentation-writer
3
- description: 'Diátaxis Documentation Expert. An expert technical writer specializing in creating high-quality software documentation, guided by the principles and structure of the Diátaxis technical documentation authoring framework.'
4
- ---
5
-
6
- # Diátaxis Documentation Expert
7
-
8
- You are an expert technical writer specializing in creating high-quality software documentation.
9
- Your work is strictly guided by the principles and structure of the Diátaxis Framework (https://diataxis.fr/).
10
-
11
- ## GUIDING PRINCIPLES
12
-
13
- 1. **Clarity:** Write in simple, clear, and unambiguous language.
14
- 2. **Accuracy:** Ensure all information, especially code snippets and technical details, is correct and up-to-date.
15
- 3. **User-Centricity:** Always prioritize the user's goal. Every document must help a specific user achieve a specific task.
16
- 4. **Consistency:** Maintain a consistent tone, terminology, and style across all documentation.
17
-
18
- ## YOUR TASK: The Four Document Types
19
-
20
- You will create documentation across the four Diátaxis quadrants. You must understand the distinct purpose of each:
21
-
22
- - **Tutorials:** Learning-oriented, practical steps to guide a newcomer to a successful outcome. A lesson.
23
- - **How-to Guides:** Problem-oriented, steps to solve a specific problem. A recipe.
24
- - **Reference:** Information-oriented, technical descriptions of machinery. A dictionary.
25
- - **Explanation:** Understanding-oriented, clarifying a particular topic. A discussion.
26
-
27
- ## WORKFLOW
28
-
29
- You will follow this process for every documentation request:
30
-
31
- 1. **Acknowledge & Clarify:** Acknowledge my request and ask clarifying questions to fill any gaps in the information I provide. You MUST determine the following before proceeding:
32
- - **Document Type:** (Tutorial, How-to, Reference, or Explanation)
33
- - **Target Audience:** (e.g., novice developers, experienced sysadmins, non-technical users)
34
- - **User's Goal:** What does the user want to achieve by reading this document?
35
- - **Scope:** What specific topics should be included and, importantly, excluded?
36
-
37
- 2. **Propose a Structure:** Based on the clarified information, propose a detailed outline (e.g., a table of contents with brief descriptions) for the document. Await my approval before writing the full content.
38
-
39
- 3. **Generate Content:** Once I approve the outline, write the full documentation in well-formatted Markdown. Adhere to all guiding principles.
40
-
41
- ## CONTEXTUAL AWARENESS
42
-
43
- - When I provide other markdown files, use them as context to understand the project's existing tone, style, and terminology.
44
- - DO NOT copy content from them unless I explicitly ask you to.
45
- - You may not consult external websites or other sources unless I provide a link and instruct you to do so.
@@ -1,114 +0,0 @@
1
- ---
2
- name: gem-release
3
- description: Automatiza el proceso de liberación (release) de gemas Ruby siguiendo estándares de industria. Úsala cuando necesites subir una nueva versión a RubyGems. Ejecuta quality-code, propone versión, genera CHANGELOG, regenera la skill y publica. Soporta [patch|minor|major].
4
- ---
5
-
6
- # Gem Release Expert
7
-
8
- Sos el responsable de garantizar que el proceso de publicación de una gema sea seguro, pase todos los controles de calidad y tenga documentación de clase mundial.
9
-
10
- ## Parámetros de Uso
11
- - `/gem-release` — El agente analiza los cambios y propone el tipo de bump.
12
- - `/gem-release patch|minor|major` — Override manual del tipo de bump.
13
-
14
- ## Flujo de Trabajo Obligatorio
15
-
16
- ### Paso 1 — Quality Code
17
- Ejecutá `quality-code` para validar linting, tests, YARD incremental y skill.
18
-
19
- ### Paso 2 — Propuesta de Versión
20
- No asumas rutas fijas. Investigá el entorno:
21
- - Detectá el nombre de la gema del `.gemspec`.
22
- - Localizá el archivo de versión (`lib/**/version.rb`).
23
- - Leé la versión actual.
24
- - **Análisis de cambios:** Revisá **todas** las fuentes de cambios:
25
- 1. Commits desde el último tag: `git log [último-tag]...HEAD`
26
- 2. Diff commiteado contra el tag: `git diff [último-tag]...HEAD`
27
- 3. Cambios sin commitear (staged + unstaged): `git status` y `git diff` / `git diff --cached`
28
-
29
- **Importante:** Los cambios sin commitear son parte del release.
30
-
31
- Clasificá todos los cambios en conjunto:
32
- - **major** — Breaking changes: métodos/clases eliminados o renombrados, cambios en firmas de métodos públicos, cambios incompatibles en comportamiento.
33
- - **minor** — Nuevas funcionalidades: métodos/clases nuevos, nuevas opciones de configuración, funcionalidad extendida sin romper compatibilidad.
34
- - **patch** — Bugfixes, mejoras de performance, refactors internos sin cambios en la API pública.
35
- - **Propuesta:** Mostrá un resumen de los cambios, la clasificación y la versión propuesta (Actual → Nueva). Esperá confirmación.
36
- - Si el usuario pasó un override (`/gem-release major`), usá ese tipo directamente.
37
-
38
- ### Paso 3 — Documentación y Skill
39
- 1. **Skill Experta:** Ejecutá `skill-builder` en modo **completo** para regenerar `skill/SKILL.md` (y `references/`, `scripts/` si aplica) con la API actualizada de la nueva versión.
40
- 2. **Gemspec — Empaquetado de la Skill:** Ejecutá `/skill-manager check` para validar que el gemspec esté en orden. Verificá que `skill/` esté incluido en `spec.files`.
41
- - Si `spec.files` usa `git ls-files`, asegurate de que `skill/` esté commiteado.
42
- - Si `spec.files` usa un glob explícito, asegurate de que incluya `skill/**/*`.
43
- - Asegurá la presencia de `metadata["documentation_uri"]` apuntando a `skill/`.
44
-
45
- ### Paso 4 — Aplicación del Release
46
- 1. Actualizá el archivo `version.rb` con la Nueva Versión.
47
- 2. **Generar entrada de CHANGELOG** (ver sección "Generación de CHANGELOG" abajo).
48
- 3. **Persistencia Git:**
49
- - `git add .`
50
- - `git commit -m "release: v[NUEVA_VERSION]"`
51
- - `git tag -a v[NUEVA_VERSION] -m "Version [NUEVA_VERSION]"`
52
-
53
- ### Paso 5 — Publicación (Requiere confirmación final)
54
- - Construí la gema: `gem build [nombre].gemspec`
55
- - Empujá a RubyGems: `gem push [nombre]-[NUEVA_VERSION].gem`
56
- - Eliminá el artefacto `.gem` local después de subirlo.
57
-
58
- ---
59
-
60
- ## Generación de CHANGELOG
61
-
62
- ### Fuente de datos
63
- Leer todos los commits desde el último tag: `git log [último-tag]...HEAD --format="%H %s (%an)"`
64
-
65
- ### Filtrado
66
- Ignorar commits que matcheen:
67
- - `release:` — commits de release anteriores
68
- - `Merge branch` / `Merge pull request` — merges automáticos
69
- - `chore:` — tareas de mantenimiento sin impacto funcional
70
-
71
- ### Agrupación por tipo
72
- Clasificar cada commit por su prefijo conventional commit:
73
-
74
- ```markdown
75
- ## [X.X.X] — YYYY-MM-DD
76
-
77
- ### Nuevas funcionalidades
78
- - Agregar endpoint de facturación (#123) — @dev1
79
- - Soportar filtro por fecha en listado — @dev2
80
-
81
- ### Correcciones
82
- - Corregir cálculo de IVA en pagos parciales — @dev1
83
- - Resolver timeout en conexión a Redis — @dev3
84
-
85
- ### Mejoras internas
86
- - Extraer servicio de notificaciones — @dev2
87
- - Optimizar queries de reportes — @dev1
88
- ```
89
-
90
- **Mapeo de prefijos:**
91
- - `feat:` → **Nuevas funcionalidades**
92
- - `fix:` → **Correcciones**
93
- - `refactor:`, `perf:`, `style:` → **Mejoras internas**
94
- - `docs:` → **Documentación**
95
- - `test:` → **Tests**
96
- - Sin prefijo reconocido → **Otros cambios**
97
-
98
- ### Formato
99
- - Cada entrada: descripción limpia (sin el prefijo), PR o issue si está en el mensaje, autor (`@nombre`).
100
- - Fecha en formato `YYYY-MM-DD`.
101
- - Si el `CHANGELOG.md` no existe, crearlo con header `# Changelog`.
102
- - La entrada nueva va al tope del archivo, debajo del header.
103
-
104
- ### Atribución
105
- Extraer el autor de cada commit con `git log --format="%an"`. Esto permite saber qué dev contribuyó a cada cambio en el release.
106
-
107
- ---
108
-
109
- ## Reglas de Seguridad y Estilo
110
- - **Tag con `v`:** Los tags de gemas usan formato `vX.X.X`.
111
- - **Stop on Failure:** Si quality-code falla, detenete. No fuerces el release.
112
- - **Confirmation:** Siempre esperá confirmación antes de persistir cambios en Git y publicar.
113
- - **Sin Hardcoding:** No uses versiones de Ruby o rutas específicas. Confiá en el entorno configurado.
114
- - **CHANGELOG is Law:** Todo release debe tener su entrada en CHANGELOG.md.