bug_bunny 4.16.0 → 4.17.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 67e58763b6f088c44e17121922ecb618c9ccef8738096d534c578eea021c5f94
4
- data.tar.gz: 47fe7485dd056e9ffb8a7e5268b97e4d0173e0441984506b5528f5c43643da7a
3
+ metadata.gz: ec5013f4d782766d388826265255b008a9c17119b15acef95a05fcb01a9722d5
4
+ data.tar.gz: 0cd930d1d4c982a0ba9bf73e9562504d0e776c88c760e427dc440a1399b57a13
5
5
  SHA512:
6
- metadata.gz: f0a5927c4a4a450b414ee38ffbc25a0a97a80b2678d32a19d524a51367572f7b5e2935e29a81bf9afbdd2cfb0b634e9b60a322b828ff14ba0f79e60982148723
7
- data.tar.gz: e66726447226ceb96f2d3b81dd759209fd8e54e7ea67bb85870cc805e44747e81f8121a821fd7a6fa65435ae3ba95e3cd62631ef0687fa0543f29848e21f4682
6
+ metadata.gz: 800c22370b7e39812cc1d90ca74db9ea4736e4354e4061d39a0e0e9d5fbb7fa5ff05c56474f61056a7fe072144339c48d725f040c34c16ba7f0805a314881d66
7
+ data.tar.gz: 921e0359f5426db49beaf327e67885568313282c3972e9bc2e9d7f88e28fdad032f3258ab73e4cca16f9b3f70cdbf5f42311e3d4f910e4430494ebb4ea8e05a8
data/CHANGELOG.md CHANGED
@@ -1,5 +1,43 @@
1
1
  # Changelog
2
2
 
3
+ ## [4.17.0] - 2026-05-13
4
+
5
+ ### Nuevas funcionalidades
6
+ - **`Client::REQUEST_ATTRS` extendido con metadata AMQP estándar (#45):** Los siguientes atributos del Request ahora pueden pasarse como kwargs directos en `client.publish` / `client.request` sin necesidad del block API:
7
+ - `persistent` (Boolean) — `delivery_mode: 2` AMQP. Critical: por default es `false`; `confirmed: true` **NO** lo implica.
8
+ - `correlation_id` (String) — Tracing explícito (sobreescribe el auto-asignado por RPC y `return_raise`).
9
+ - `priority` (Integer 0-255).
10
+ - `app_id` (String).
11
+ - `content_type` (String, default `'application/json'`).
12
+ - `content_encoding` (String).
13
+ - `expiration` (String, TTL en ms).
14
+
15
+ ```ruby
16
+ # Antes (4.16): requería block API
17
+ client.publish('evt', exchange: 'x', confirmed: true) do |req|
18
+ req.persistent = true
19
+ req.correlation_id = 'cid-123'
20
+ end
21
+
22
+ # Ahora (4.17): kwargs directos
23
+ client.publish('evt', exchange: 'x', confirmed: true,
24
+ persistent: true, correlation_id: 'cid-123')
25
+ ```
26
+
27
+ El block API sigue funcionando para overrides puntuales o para atributos no expuestos (`timestamp`, `type`, `reply_to`).
28
+
29
+ ### Correcciones
30
+ - **`apply_args` usa `args.key?` en lugar de truthy check:** Permite pasar valores falsy explícitos (ej. `persistent: false`, `priority: 0`) que antes se filtraban silenciosamente como si no se hubieran pasado.
31
+
32
+ ### Documentación (motivado por #45 — adopción real en sequre/box_radius_manager#18)
33
+ - README + SKILL.md cubren cuatro gotchas detectados solo en integration tests:
34
+ 1. `url` es positional, no kwarg `:path`. Splatear un hash con `path:` rompe.
35
+ 2. `confirmed: true` no implica `persistent: true` — son flags ortogonales.
36
+ 3. Default `exchange_options` es `{ durable: false }` — publishers a exchange compartido deben pasar `exchange_options: { durable: true }` explícito.
37
+ 4. `instance_double` no detecta arity mismatch con splat de kwargs — recomendación de smoke test integration para cada publisher nuevo.
38
+ - Nueva sección "Production publisher recipe" en README y receta canónica en SKILL.md con la combinación recomendada para auditoría/billing/accounting.
39
+ - `skill/references/client-middleware.md` con tabla completa de kwargs de Request actualizada.
40
+
3
41
  ## [4.16.0] - 2026-05-13
4
42
 
5
43
  ### Cambios de comportamiento (semi-breaking)
data/README.md CHANGED
@@ -210,6 +210,73 @@ client.publish('events', body: { type: 'user.signed_in', user_id: 42 })
210
210
  client.request('users', method: :get, params: { role: 'admin', page: 2 })
211
211
  ```
212
212
 
213
+ ### Gotchas
214
+
215
+ **URL is positional, not a kwarg.** The first argument of `client.request` / `client.publish` is positional. There is **no** `path:` kwarg, splatting a hash with `path:` will fail silently or raise `ArgumentError`:
216
+
217
+ ```ruby
218
+ args = { exchange: 'ingest.x', body: payload }
219
+ client.publish(**args) # ❌ ArgumentError: wrong number of arguments
220
+ client.publish('event.name', **args) # ✅
221
+ ```
222
+
223
+ **Block runs after kwargs.** Keyword args are applied first; the block (if given) can override them. Use kwargs for the common case and block for atypical setup:
224
+
225
+ ```ruby
226
+ client.publish('evt', exchange: 'x', persistent: true) do |req|
227
+ req.timestamp = some_past_time # only via block — not in REQUEST_ATTRS
228
+ end
229
+ ```
230
+
231
+ ### Production publisher recipe
232
+
233
+ Defaults aimed at sane microservices — declare durable exchanges, persistent messages, confirmed delivery with mandatory routing, explicit correlation id:
234
+
235
+ ```ruby
236
+ client.publish('acct.start',
237
+ exchange: 'ingest.radius',
238
+ exchange_type: :topic,
239
+ exchange_options: { durable: true }, # match consumer-declared exchange
240
+ body: payload,
241
+ confirmed: true,
242
+ mandatory: true, # raise PublishUnroutable if no binding
243
+ persistent: true, # delivery_mode: 2 — survives broker restart
244
+ correlation_id: SecureRandom.uuid,
245
+ app_id: 'radius_manager')
246
+ ```
247
+
248
+ | AMQP property | Kwarg | Reason it matters for critical publishers |
249
+ |---|---|---|
250
+ | `delivery_mode` | `persistent: true` | Without it, the message lives only in broker RAM (lost on restart). Default `false`. |
251
+ | `confirmation` | `confirmed: true` | Block until the broker acks. Without it, `client.publish` returns 202 before the broker sees the message. |
252
+ | `mandatory` | `mandatory: true` | Catches misrouted publishes. Combined with `return_raise` (default `true`), raises `PublishUnroutable` instead of silently dropping. |
253
+ | `exchange durable` | `exchange_options: { durable: true }` | Match the exchange definition that consumers declare. Mismatch raises `Bunny::PreconditionFailed`. |
254
+ | `correlation_id` | `correlation_id:` | Tracing. Auto-generated when missing for RPC and for `confirmed + mandatory + return_raise`, but explicit is preferred. |
255
+
256
+ ### Testing publishers
257
+
258
+ Mocks of `Client` (via `instance_double`) **do not catch arity mismatches** when the caller does splat (`**args`). Signature errors like passing `path:` as kwarg or unknown keys won't surface in unit tests with mocks. **Add a smoke integration test** for new publishers — declare an exclusive queue, bind to the exchange, publish, `queue.pop`, assert `correlation_id`, `headers`, `routing_key`, `delivery_mode`:
259
+
260
+ ```ruby
261
+ RSpec.describe 'MyPublisher', :integration do
262
+ it 'publishes with correct AMQP metadata' do
263
+ conn = BugBunny.create_connection
264
+ ch = conn.create_channel
265
+ x = ch.topic('ingest.radius', durable: true)
266
+ q = ch.queue('', exclusive: true).bind(x, routing_key: 'acct.#')
267
+
268
+ MyPublisher.call(payload)
269
+
270
+ _delivery, props, body = q.pop(manual_ack: false)
271
+ expect(props.correlation_id).not_to be_nil
272
+ expect(props.delivery_mode).to eq(2) # persistent
273
+ expect(JSON.parse(body)).to include(...)
274
+ ensure
275
+ conn&.close
276
+ end
277
+ end
278
+ ```
279
+
213
280
  ### Publisher Confirms (delivery-critical events)
214
281
 
215
282
  For events where you need a delivery guarantee from the broker (auditing, billing, accounting) without the cost of a full RPC, use `publish` with `confirmed: true`. The call blocks until the broker acknowledges receipt:
@@ -29,9 +29,17 @@ module BugBunny
29
29
  attr_accessor :delivery_mode
30
30
 
31
31
  # Argumentos del cliente que se mapean 1:1 a setters del Request.
32
+ #
33
+ # Incluye opciones de enrutamiento (`delivery_mode`, `method`, `body`, `exchange`,
34
+ # `exchange_type`, `routing_key`, `timeout`, `params`), de infraestructura
35
+ # (`exchange_options`, `queue_options`) y de metadata AMQP estándar
36
+ # (`persistent`, `correlation_id`, `priority`, `app_id`, `content_type`,
37
+ # `content_encoding`, `expiration`). Lo que no esté acá debe setearse via
38
+ # block API (`do |req| ... end`).
32
39
  REQUEST_ATTRS = %i[
33
40
  delivery_mode method body exchange exchange_type routing_key
34
41
  timeout exchange_options queue_options params
42
+ persistent correlation_id priority app_id content_type content_encoding expiration
35
43
  ].freeze
36
44
 
37
45
  # Inicializa un nuevo cliente.
@@ -64,7 +72,8 @@ module BugBunny
64
72
  #
65
73
  # Envía un mensaje y bloquea la ejecución del hilo actual hasta recibir respuesta.
66
74
  #
67
- # @param url [String] La ruta del recurso (ej: 'users/1').
75
+ # @param url [String] La ruta del recurso (ej: 'users/1'). **Argumento posicional** —
76
+ # no existe el kwarg `:path`. Splatear un hash con `path:` falla silencioso.
68
77
  # @param args [Hash] Opciones de configuración.
69
78
  # @option args [Symbol] :method El verbo HTTP (:get, :post, :put, :delete). Default: :get.
70
79
  # @option args [Object] :body El cuerpo del mensaje.
@@ -72,7 +81,18 @@ module BugBunny
72
81
  # @option args [Integer] :timeout Tiempo máximo de espera.
73
82
  # @option args [Hash] :exchange_options Opciones específicas para la declaración del Exchange.
74
83
  # @option args [Hash] :queue_options Opciones específicas para la declaración de la Cola.
75
- # @yield [req] Bloque para configurar el objeto Request directamente.
84
+ # @option args [Boolean] :persistent Si `true`, `delivery_mode: 2` (mensaje persiste en disco
85
+ # del broker). Default `false`. Útil para mensajes críticos sobre queues durables.
86
+ # @option args [String] :correlation_id ID de correlación AMQP. Útil para tracing custom.
87
+ # Si no se setea, `Producer#rpc` y `Producer#confirmed` (con return_raise) auto-asignan UUID.
88
+ # @option args [Integer] :priority Prioridad del mensaje (0-255). Requiere queue declarada con
89
+ # `x-max-priority` para que el broker la respete.
90
+ # @option args [String] :app_id Identificador del publisher.
91
+ # @option args [String] :content_type MIME type. Default `'application/json'`.
92
+ # @option args [String] :content_encoding Encoding del payload (ej: 'gzip').
93
+ # @option args [String] :expiration TTL del mensaje en ms (formato AMQP).
94
+ # @yield [req] Bloque para configurar el objeto Request directamente. Necesario para
95
+ # atributos no mapeados como kwarg (ej: `req.timestamp = ...`).
76
96
  # @return [Hash] La respuesta del servidor.
77
97
  def request(url, **args)
78
98
  send(url, **args) do |req|
@@ -87,7 +107,8 @@ module BugBunny
87
107
  # que el broker confirme la recepción del mensaje. Útil para eventos críticos (auditoría,
88
108
  # billing) donde se requiere garantía de entrega sin el overhead de un RPC completo.
89
109
  #
90
- # @param url [String] La ruta del evento/recurso.
110
+ # @param url [String] La ruta del evento/recurso. **Argumento posicional** — no
111
+ # existe el kwarg `:path`. Splatear un hash con `path:` falla silencioso.
91
112
  # @param args [Hash] Mismas opciones que {#request}, excepto `:timeout`. Adicionales:
92
113
  # @option args [Boolean] :confirmed Si `true`, espera `wait_for_confirms` del broker.
93
114
  # @option args [Boolean] :mandatory Si `true`, el broker retorna el mensaje si no es ruteable.
@@ -158,12 +179,16 @@ module BugBunny
158
179
 
159
180
  # Mapea los argumentos generales (no específicos de Publisher Confirms) sobre el Request.
160
181
  #
182
+ # Usa `args.key?` en lugar de truthy check para que `persistent: false`,
183
+ # `priority: 0` u otros valores falsy explícitos del caller sean honrados
184
+ # (no se filtran como si no hubieran sido pasados).
185
+ #
161
186
  # @param req [BugBunny::Request]
162
187
  # @param args [Hash]
163
188
  # @return [void]
164
189
  def apply_args(req, args)
165
190
  REQUEST_ATTRS.each do |key|
166
- req.public_send("#{key}=", args[key]) if args[key]
191
+ req.public_send("#{key}=", args[key]) if args.key?(key)
167
192
  end
168
193
  req.headers.merge!(args[:headers]) if args[:headers]
169
194
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module BugBunny
4
- VERSION = '4.16.0'
4
+ VERSION = '4.17.0'
5
5
  end
data/skill/SKILL.md CHANGED
@@ -245,6 +245,21 @@ client.publish('acct.start', exchange: 'acct_x', body: payload,
245
245
  # → { 'status' => 202, 'body' => nil } # broker ACK confirmado
246
246
  ```
247
247
 
248
+ **Receta canónica de publisher productivo (auditoría / billing / accounting):**
249
+ ```ruby
250
+ client.publish('acct.start',
251
+ exchange: 'ingest.radius',
252
+ exchange_type: :topic,
253
+ exchange_options: { durable: true }, # matchear declaración del consumer
254
+ body: payload,
255
+ confirmed: true, # broker ACK síncrono
256
+ mandatory: true, # raise PublishUnroutable si no hay binding
257
+ persistent: true, # delivery_mode: 2 — sobrevive restart
258
+ correlation_id: SecureRandom.uuid, # tracing explícito
259
+ app_id: 'radius_manager')
260
+ ```
261
+ A partir de 4.17, `persistent`, `correlation_id`, `priority`, `app_id`, `content_type`, `content_encoding` y `expiration` están en `Client::REQUEST_ATTRS` y se aceptan como kwargs. El block API sigue funcionando para overrides puntuales o para atributos no expuestos (`timestamp`, `type`).
262
+
248
263
  **Dos señales del broker, dos excepciones simétricas:**
249
264
 
250
265
  | Señal | Default | Excepción | Campos |
@@ -311,6 +326,29 @@ No guardar el resultado de `Order.with(...)` en una variable para múltiples lla
311
326
  ### Registrar middleware durante call()
312
327
  No registrar consumer middlewares durante la ejecución de `call()`. El stack toma un snapshot al inicio; los registros concurrentes no afectan la ejecución actual.
313
328
 
329
+ ### Pasar `path:` como kwarg a `Client#publish` / `#request`
330
+ El primer argumento es **posicional** (`url`). No hay kwarg `:path`. Splatear un hash que tenga `path:` falla con `ArgumentError: wrong number of arguments`. Construir args sin path y pasar la URL aparte:
331
+ ```ruby
332
+ args = { exchange: 'x', body: payload }
333
+ client.publish('event.name', **args) # ✅
334
+ client.publish(**args.merge(path: 'event.name')) # ❌
335
+ ```
336
+
337
+ ### Asumir que `confirmed: true` implica persistencia
338
+ `confirmed: true` solo activa Publisher Confirms (broker ACK síncrono). **NO** setea `delivery_mode: 2`. El default de `Request#persistent` es `false` — el mensaje vive en RAM del broker y se pierde si reinicia. Para eventos críticos sobre queue durable hay que pasar `persistent: true` (a partir de 4.17) o setearlo via block. Tabla de decisión:
339
+
340
+ | Necesitás | Pasar |
341
+ |---|---|
342
+ | Broker confirma recepción | `confirmed: true` |
343
+ | Mensaje sobrevive restart | `persistent: true` (requiere queue `durable: true`) |
344
+ | Raise si no rutea | `mandatory: true` (+ `return_raise: true` default) |
345
+
346
+ ### Olvidar `exchange_options: { durable: true }` en publishers a exchange compartido
347
+ `DEFAULT_EXCHANGE_OPTIONS = { durable: false, auto_delete: false }`. Si un consumer previamente declaró el exchange como durable (caso normal en producción), un publisher que use el default va a recibir `Bunny::PreconditionFailed - inequivalent arg 'durable'` al re-declarar. Solución: pasar `exchange_options: { durable: true }` en el publisher, o setear global `BugBunny.configure { |c| c.exchange_options = { durable: true } }`.
348
+
349
+ ### Confiar en `instance_double(BugBunny::Client)` para detectar errores de signature
350
+ Limitación de RSpec: `instance_double` valida que el método exista pero **no** valida arity estricta cuando el caller hace `**args` splat. Tests con mocks pasan, integration con broker real rompe. Mitigación: para cada publisher nuevo, sumar un smoke spec `:integration` que declare queue exclusiva con binding, publique, haga `queue.pop`, y verifique `correlation_id`, `delivery_mode`, `headers`, `routing_key`.
351
+
314
352
  ---
315
353
 
316
354
  ## Errores Comunes
@@ -47,6 +47,18 @@ end
47
47
  | `mandatory` | Boolean | `false` | Pide al broker retornar el mensaje si no es ruteable. Solo útil con `confirmed: true`. |
48
48
  | `confirm_timeout` | Float | `nil` | Segundos máximos a esperar el ACK. `nil` = espera indefinida. Excedido → `BugBunny::RequestTimeout`. |
49
49
  | `nack_raise` | Boolean | `nil` | Override per-request de `config.nack_raise`. `nil` = usa flag global. |
50
+ | `return_raise` | Boolean | `nil` | Override per-request de `config.return_raise`. Requiere `confirmed: true` y `mandatory: true`. |
51
+ | `persistent` | Boolean | `false` | `delivery_mode: 2` AMQP. Mensaje sobrevive restart del broker. Requiere queue durable. |
52
+ | `correlation_id` | String | nil | ID de correlación AMQP. Auto-asignado UUID en RPC y en `confirmed + mandatory + return_raise`. |
53
+ | `priority` | Integer | nil | Prioridad 0-255. Requiere queue con `x-max-priority`. |
54
+ | `app_id` | String | nil | Identificador del publisher (AMQP `app-id`). |
55
+ | `content_type` | String | `'application/json'` | MIME type del payload. |
56
+ | `content_encoding` | String | nil | Encoding del payload (`'gzip'`, `'deflate'`, etc.). |
57
+ | `expiration` | String | nil | TTL del mensaje en ms (formato AMQP). |
58
+
59
+ **Gotcha:** el primer argumento de `Client#publish` / `#request` es **posicional** (`url`). No existe el kwarg `:path`. Splatear un hash con `path:` falla con `ArgumentError` o se ignora silencioso.
60
+
61
+ **Atributos no expuestos como kwarg** (solo via block API): `timestamp` (default `Time.now.to_i`), `type` (default `full_path`), `reply_to` (RPC interno).
50
62
 
51
63
  ## Producer (bajo nivel)
52
64
 
@@ -287,6 +287,108 @@ RSpec.describe BugBunny::Client, 'session pooling' do
287
287
  end
288
288
  end
289
289
 
290
+ describe 'AMQP metadata kwargs (REQUEST_ATTRS extension)' do
291
+ let(:fake_exchange) { double('exchange', publish: nil) }
292
+
293
+ def stub_producer_capture
294
+ captured = nil
295
+ allow_any_instance_of(BugBunny::Producer).to receive(:fire) do |_prod, req|
296
+ captured = req
297
+ { 'status' => 202, 'body' => nil }
298
+ end
299
+ allow_any_instance_of(BugBunny::Producer).to receive(:confirmed) do |_prod, req|
300
+ captured = req
301
+ { 'status' => 202, 'body' => nil }
302
+ end
303
+ [-> { captured }]
304
+ end
305
+
306
+ it 'persistent: true se propaga al Request via kwarg' do
307
+ client = described_class.new(pool: fake_pool(fake_conn))
308
+ get_req, = stub_producer_capture
309
+
310
+ client.publish('evt', exchange: 'x', exchange_type: 'direct', persistent: true)
311
+
312
+ expect(get_req.call.persistent).to be(true)
313
+ end
314
+
315
+ it 'persistent: false explícito se honra (no se filtra como falsy)' do
316
+ client = described_class.new(pool: fake_pool(fake_conn))
317
+ get_req, = stub_producer_capture
318
+
319
+ client.publish('evt', exchange: 'x', exchange_type: 'direct', persistent: false)
320
+
321
+ expect(get_req.call.persistent).to be(false)
322
+ end
323
+
324
+ it 'correlation_id: se propaga al Request via kwarg' do
325
+ client = described_class.new(pool: fake_pool(fake_conn))
326
+ get_req, = stub_producer_capture
327
+
328
+ client.publish('evt', exchange: 'x', exchange_type: 'direct', correlation_id: 'cid-123')
329
+
330
+ expect(get_req.call.correlation_id).to eq('cid-123')
331
+ end
332
+
333
+ it 'priority: se propaga al Request via kwarg' do
334
+ client = described_class.new(pool: fake_pool(fake_conn))
335
+ get_req, = stub_producer_capture
336
+
337
+ client.publish('evt', exchange: 'x', exchange_type: 'direct', priority: 9)
338
+
339
+ expect(get_req.call.priority).to eq(9)
340
+ end
341
+
342
+ it 'app_id, content_type, content_encoding, expiration se propagan via kwargs' do
343
+ client = described_class.new(pool: fake_pool(fake_conn))
344
+ get_req, = stub_producer_capture
345
+
346
+ client.publish('evt',
347
+ exchange: 'x', exchange_type: 'direct',
348
+ app_id: 'radius_manager',
349
+ content_type: 'application/x-protobuf',
350
+ content_encoding: 'gzip',
351
+ expiration: '60000')
352
+
353
+ req = get_req.call
354
+ expect(req.app_id).to eq('radius_manager')
355
+ expect(req.content_type).to eq('application/x-protobuf')
356
+ expect(req.content_encoding).to eq('gzip')
357
+ expect(req.expiration).to eq('60000')
358
+ end
359
+
360
+ it 'block API sigue funcionando para atributos no expuestos como kwarg (ej: timestamp, type)' do
361
+ client = described_class.new(pool: fake_pool(fake_conn))
362
+ get_req, = stub_producer_capture
363
+
364
+ ts = Time.now.to_i - 100
365
+ client.publish('evt', exchange: 'x', exchange_type: 'direct') do |req|
366
+ req.timestamp = ts
367
+ req.type = 'custom.type'
368
+ end
369
+
370
+ req = get_req.call
371
+ expect(req.timestamp).to eq(ts)
372
+ expect(req.type).to eq('custom.type')
373
+ end
374
+
375
+ it 'kwargs y block API coexisten — block corre después y puede sobrescribir' do
376
+ client = described_class.new(pool: fake_pool(fake_conn))
377
+ get_req, = stub_producer_capture
378
+
379
+ client.publish('evt',
380
+ exchange: 'x', exchange_type: 'direct',
381
+ persistent: false, priority: 1) do |req|
382
+ req.persistent = true
383
+ req.priority = 9
384
+ end
385
+
386
+ req = get_req.call
387
+ expect(req.persistent).to be(true)
388
+ expect(req.priority).to eq(9)
389
+ end
390
+ end
391
+
290
392
  describe 'Session no se cierra entre requests' do
291
393
  it 'no invoca close en la Session al terminar el request' do
292
394
  conn = fake_conn
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: bug_bunny
3
3
  version: !ruby/object:Gem::Version
4
- version: 4.16.0
4
+ version: 4.17.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - gabix
@@ -304,7 +304,7 @@ metadata:
304
304
  homepage_uri: https://github.com/gedera/bug_bunny
305
305
  source_code_uri: https://github.com/gedera/bug_bunny
306
306
  changelog_uri: https://github.com/gedera/bug_bunny/blob/main/CHANGELOG.md
307
- documentation_uri: https://github.com/gedera/bug_bunny/blob/v4.16.0/skill
307
+ documentation_uri: https://github.com/gedera/bug_bunny/blob/v4.17.0/skill
308
308
  post_install_message:
309
309
  rdoc_options: []
310
310
  require_paths: