otto 2.9.0 → 2.10.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 (96) hide show
  1. checksums.yaml +4 -4
  2. data/.github/dependabot.yml +5 -0
  3. data/.github/workflows/ci.yml +11 -8
  4. data/.github/workflows/claude-code-review.yml +38 -13
  5. data/.github/workflows/claude.yml +10 -8
  6. data/.github/workflows/code-smells.yml +5 -5
  7. data/.github/workflows/release-gem.yml +2 -2
  8. data/.github/workflows/ruby-lint.yml +3 -3
  9. data/.github/workflows/yardoc.yml +5 -5
  10. data/.gitignore +1 -5
  11. data/.rubocop_todo.yml +7 -5
  12. data/AGENTS.md +22 -1
  13. data/CHANGELOG.rst +203 -0
  14. data/Gemfile +3 -3
  15. data/Gemfile.lock +11 -15
  16. data/README.md +75 -37
  17. data/docs/README.md +166 -0
  18. data/docs/adr/README.md +16 -0
  19. data/docs/adr/adr-001-route-authentication-at-handler-boundary.md +38 -0
  20. data/docs/adr/adr-002-multi-strategy-authentication-and-authorization.md +50 -0
  21. data/docs/adr/adr-003-caddy-tls-route-based-integration.md +48 -0
  22. data/docs/adr/adr-004-separate-compatibility-from-security-maintenance.md +58 -0
  23. data/docs/guides/authentication.md +377 -0
  24. data/docs/guides/caddy-tls.md +205 -0
  25. data/docs/guides/configuration_freezing.md +146 -0
  26. data/docs/guides/enrichment.md +161 -0
  27. data/docs/guides/forwarded-authority.md +249 -0
  28. data/docs/guides/geo-country.md +168 -0
  29. data/docs/guides/ip_privacy.md +39 -0
  30. data/docs/guides/ipaddr-encoding-quirk.md +56 -0
  31. data/docs/guides/mcp.md +282 -0
  32. data/docs/guides/privacy.md +193 -0
  33. data/docs/guides/routing.md +181 -0
  34. data/docs/guides/structured_logging.md +281 -0
  35. data/docs/guides/testing-guide.md +391 -0
  36. data/docs/maintainers/github-actions.md +41 -0
  37. data/docs/maintainers/investigations/.gitignore +2 -0
  38. data/docs/migrating/v2.0.0.md +337 -0
  39. data/docs/reference/authentication.md +290 -0
  40. data/docs/reference/route-syntax.md +181 -0
  41. data/docs/reference/runtime-and-dependency-security.md +86 -0
  42. data/examples/advanced_routes/README.md +43 -57
  43. data/examples/authentication_strategies/README.md +37 -196
  44. data/examples/basic/README.md +24 -39
  45. data/examples/basic/config.ru +0 -1
  46. data/examples/caddy_tls_demo/README.md +8 -2
  47. data/examples/lambda_handlers/README.md +11 -2
  48. data/examples/mcp_demo/README.md +75 -161
  49. data/examples/mcp_demo/config.ru +1 -0
  50. data/examples/security_features/README.md +36 -234
  51. data/lib/otto/caddy_tls/localhost_guard.rb +22 -24
  52. data/lib/otto/core/configuration.rb +36 -22
  53. data/lib/otto/core/file_safety.rb +89 -31
  54. data/lib/otto/core/middleware_stack.rb +36 -36
  55. data/lib/otto/core/router.rb +62 -19
  56. data/lib/otto/env_keys.rb +20 -2
  57. data/lib/otto/mcp/auth/token.rb +10 -4
  58. data/lib/otto/mcp/core.rb +23 -5
  59. data/lib/otto/mcp/endpoint.rb +41 -0
  60. data/lib/otto/mcp/errors.rb +15 -0
  61. data/lib/otto/mcp/options.rb +292 -0
  62. data/lib/otto/mcp/protocol.rb +52 -22
  63. data/lib/otto/mcp/rate_limiting.rb +175 -97
  64. data/lib/otto/mcp/registry.rb +14 -14
  65. data/lib/otto/mcp/schema_validation.rb +20 -12
  66. data/lib/otto/mcp/server.rb +131 -32
  67. data/lib/otto/optional_dependency.rb +57 -0
  68. data/lib/otto/privacy/config.rb +10 -8
  69. data/lib/otto/security/authentication/route_auth_wrapper/role_authorization.rb +22 -5
  70. data/lib/otto/security/authentication/strategies/api_key_strategy.rb +181 -18
  71. data/lib/otto/security/authentication/strategies/permission_strategy.rb +1 -0
  72. data/lib/otto/security/authentication/strategies/role_strategy.rb +1 -0
  73. data/lib/otto/security/authentication/strategies/session_strategy.rb +1 -0
  74. data/lib/otto/security/authentication/strategy_result.rb +58 -28
  75. data/lib/otto/security/config.rb +328 -14
  76. data/lib/otto/security/configurator.rb +36 -6
  77. data/lib/otto/security/core.rb +19 -1
  78. data/lib/otto/security/middleware/ip_privacy_middleware.rb +105 -4
  79. data/lib/otto/security/middleware/rate_limit_middleware.rb +1 -6
  80. data/lib/otto/security/rate_limiter.rb +81 -46
  81. data/lib/otto/utils.rb +50 -0
  82. data/lib/otto/version.rb +1 -1
  83. data/lib/otto.rb +9 -11
  84. data/otto.gemspec +0 -2
  85. metadata +31 -41
  86. data/docs/.gitignore +0 -10
  87. data/docs/1108-STREAMING_ARCHITECTURE_ANALYSIS.md +0 -1105
  88. data/docs/1108-STREAMING_SUPPORT_SUMMARY.md +0 -376
  89. data/docs/enrichment.md +0 -128
  90. data/docs/geo-country.md +0 -181
  91. data/docs/ipaddr-encoding-quirk.md +0 -34
  92. data/docs/migrating/v2.0.0-pre1.md +0 -276
  93. data/docs/migrating/v2.0.0-pre2.md +0 -338
  94. data/docs/modern-authentication-authorization-landscape.md +0 -558
  95. data/docs/multi-strategy-authentication-design.md +0 -1401
  96. data/docs/reverse-proxy-network-services.md +0 -371
@@ -0,0 +1,391 @@
1
+ # Testing Otto applications
2
+
3
+ This guide covers the smallest useful testing workflow for an Otto application.
4
+ It favors requests through a real `Otto` instance and links to Otto's maintained
5
+ specs for implementation-level details.
6
+
7
+ ## Prerequisites and commands
8
+
9
+ Otto supports Ruby `>= 3.2, < 4.1`. From an Otto source checkout, enable the
10
+ optional development and test groups before installing dependencies:
11
+
12
+ ```sh
13
+ bundle config set --local with 'development test'
14
+ bundle install
15
+ bundle exec rspec
16
+ ```
17
+
18
+ Run one file while developing:
19
+
20
+ ```sh
21
+ bundle exec rspec spec/otto/security/route_auth_wrapper_spec.rb
22
+ ```
23
+
24
+ The root `Rakefile` also makes `bundle exec rake` run the specs when RSpec is
25
+ installed. Otto's CI runs `bundle exec rspec`; use that command when checking the
26
+ same test entry point locally.
27
+
28
+ Applications consuming Otto need RSpec and `rack-test` in their own test bundle
29
+ if they use the examples below.
30
+
31
+ ## Minimal test setup
32
+
33
+ Use Rack's environment builder rather than constructing partial Rack hashes by
34
+ hand:
35
+
36
+ ```ruby
37
+ # spec/spec_helper.rb
38
+ require 'bundler/setup'
39
+ require 'json'
40
+ require 'rack/mock'
41
+ require 'rspec'
42
+ require 'tempfile'
43
+ require 'otto'
44
+
45
+ module OttoAppSpecHelpers
46
+ def rack_env(path = '/', method: 'GET', headers: {}, params: {})
47
+ env = Rack::MockRequest.env_for(path, method: method, params: params)
48
+ headers.each do |name, value|
49
+ rack_name = name.upcase.tr('-', '_')
50
+ # Rack keeps Content-Type and Content-Length unprefixed; everything else
51
+ # is HTTP_-prefixed. Without this, a JSON request never reaches Otto's
52
+ # JSON parser.
53
+ key = %w[CONTENT_TYPE CONTENT_LENGTH].include?(rack_name) ? rack_name : "HTTP_#{rack_name}"
54
+ env[key] = value
55
+ end
56
+ env
57
+ end
58
+
59
+ def build_otto(route_lines, **options)
60
+ file = Tempfile.new(['routes', '.txt'])
61
+ file.write(route_lines.join("\n") + "\n")
62
+ file.close
63
+ (@route_files ||= []) << file
64
+
65
+ Otto.new(file.path, options)
66
+ end
67
+ end
68
+
69
+ RSpec.configure do |config|
70
+ config.include OttoAppSpecHelpers
71
+
72
+ config.after do
73
+ Array(@route_files).each(&:unlink)
74
+ end
75
+ end
76
+ ```
77
+
78
+ Within Otto itself, use the existing helpers in
79
+ [`spec/support/test_helpers.rb`](../../spec/support/test_helpers.rb) instead of
80
+ copying this application-level helper.
81
+
82
+ ## Test Logic classes as plain Ruby objects
83
+
84
+ Logic classes receive an authentication result, merged parameters, and a locale.
85
+ Test business rules without a Rack request when request parsing is not part of
86
+ the behavior under test:
87
+
88
+ ```ruby
89
+ RSpec.describe Products::Show do
90
+ let(:context) do
91
+ Otto::Security::Authentication::StrategyResult.new(
92
+ session: { 'user_id' => 7 },
93
+ user: { id: 7, roles: ['customer'] },
94
+ auth_method: 'session',
95
+ metadata: {},
96
+ strategy_name: 'session'
97
+ )
98
+ end
99
+
100
+ it 'rejects a product owned by another user' do
101
+ product = instance_double(Product, owner_id: 9)
102
+ allow(Product).to receive(:find).with('42').and_return(product)
103
+
104
+ logic = described_class.new(context, { id: '42' }, 'en')
105
+
106
+ expect { logic.raise_concerns }
107
+ .to raise_error(Otto::Security::AuthorizationError)
108
+ end
109
+ end
110
+ ```
111
+
112
+ Constructing a `StrategyResult` directly is appropriate in an isolated unit
113
+ test. Application request handling should use the result Otto places in
114
+ `env['otto.strategy_result']`.
115
+
116
+ Otto invokes `raise_concerns` before `process`. For `response=json`, the JSON
117
+ handler may call an optional `response_data` formatting hook after `process`.
118
+ Do not implement `response_data` by rerunning mutating business logic.
119
+
120
+ See [`spec/otto/route_handlers_spec.rb`](../../spec/otto/route_handlers_spec.rb)
121
+ for lifecycle, parameter, locale, and JSON-body coverage.
122
+
123
+ ## Test route definitions against the current contract
124
+
125
+ `RouteDefinition` uses symbols for verbs and handler kinds. Multi-value accessors
126
+ return arrays:
127
+
128
+ ```ruby
129
+ RSpec.describe Otto::RouteDefinition do
130
+ it 'parses authentication, roles, and a Logic target' do
131
+ route = described_class.new(
132
+ 'GET',
133
+ '/admin',
134
+ 'Admin::Dashboard auth=session,api_key role=admin,editor response=json'
135
+ )
136
+
137
+ expect(route.verb).to eq(:GET)
138
+ expect(route.kind).to eq(:logic)
139
+ expect(route.klass_name).to eq('Admin::Dashboard')
140
+ expect(route.method_name).to eq('Dashboard')
141
+ expect(route.auth_requirement).to eq('session')
142
+ expect(route.auth_requirements).to eq(%w[session api_key])
143
+ expect(route.role_requirement).to eq('admin,editor')
144
+ expect(route.role_requirements).to eq(%w[admin editor])
145
+ expect(route.response_type).to eq('json')
146
+ end
147
+ end
148
+ ```
149
+
150
+ Handler kinds are `:class`, `:instance`, `:logic`, and `:lambda`. The complete
151
+ parser contract is covered by
152
+ [`spec/otto/route_definition_spec.rb`](../../spec/otto/route_definition_spec.rb).
153
+
154
+ ## Test a strategy directly
155
+
156
+ A strategy unit test should cover successful credentials, missing credentials,
157
+ and rejected credentials. Explicit credentials that were examined and rejected
158
+ should normally produce a terminal failure so a later anonymous strategy cannot
159
+ accept the request.
160
+
161
+ ```ruby
162
+ RSpec.describe Otto::Security::Authentication::Strategies::APIKeyStrategy do
163
+ subject(:strategy) { described_class.new(api_keys: ['test-key']) }
164
+
165
+ it 'authenticates the configured header value without exposing it' do
166
+ result = strategy.authenticate(
167
+ rack_env('/', headers: { 'X-API-Key' => 'test-key' }),
168
+ 'api_key'
169
+ )
170
+
171
+ expect(result).to be_authenticated
172
+ expect(result.metadata[:api_key_fingerprint]).to be_a(String)
173
+ expect(result.to_h.inspect).not_to include('test-key')
174
+ end
175
+
176
+ it 'rejects an invalid explicit key terminally' do
177
+ result = strategy.authenticate(
178
+ rack_env('/', headers: { 'X-API-Key' => 'wrong-key' }),
179
+ 'api_key'
180
+ )
181
+
182
+ expect(result).to be_a(Otto::Security::Authentication::AuthFailure)
183
+ expect(result.failure_reason).to eq('Invalid API key')
184
+ expect(result).to be_terminal
185
+ end
186
+ end
187
+ ```
188
+
189
+ Query/form API keys are ignored unless the strategy is created with
190
+ `param_name:`. Prefer header authentication because URLs are commonly logged.
191
+ The complete static-list and resolver contract is covered by
192
+ [`spec/otto/security/authentication/strategies/api_key_strategy_spec.rb`](../../spec/otto/security/authentication/strategies/api_key_strategy_spec.rb).
193
+
194
+ For a custom strategy, subclass
195
+ `Otto::Security::Authentication::AuthStrategy` and test the result returned by
196
+ `authenticate(env, requirement)`. Use `failure(reason, terminal: true)` only
197
+ when an explicit credential was presented and rejected. See the
198
+ [authentication guide](authentication.md) for the chain semantics.
199
+
200
+ ## Test a complete request through Otto
201
+
202
+ A full-stack test should exercise a real route file, handler factory,
203
+ authentication wrapper, response handler, and Rack response tuple. A registered
204
+ lambda keeps this fixture self-contained:
205
+
206
+ ```ruby
207
+ RSpec.describe 'a protected endpoint' do
208
+ let(:otto) do
209
+ app = build_otto(
210
+ ['GET /protected &protected auth=session response=json'],
211
+ lambda_handlers: {
212
+ protected: ->(_req, _res, _path_params) { { ok: true } },
213
+ }
214
+ )
215
+ app.add_auth_strategy(
216
+ 'session',
217
+ Otto::Security::Authentication::Strategies::SessionStrategy.new
218
+ )
219
+ app
220
+ end
221
+
222
+ it 'returns JSON for an authenticated session' do
223
+ env = rack_env('/protected')
224
+ env['rack.session'] = { 'user_id' => 7 }
225
+
226
+ status, headers, body = otto.call(env)
227
+
228
+ expect(status).to eq(200)
229
+ expect(headers['Content-Type']).to eq('application/json')
230
+ expect(JSON.parse(body.join)).to eq('ok' => true)
231
+ end
232
+
233
+ it 'returns a JSON 401 without a session' do
234
+ status, headers, body = otto.call(
235
+ rack_env('/protected', headers: { 'Accept' => 'application/json' })
236
+ )
237
+
238
+ expect(status).to eq(401)
239
+ expect(headers['content-type']).to eq('application/json')
240
+ expect(JSON.parse(body.join)['error']).to eq('Authentication Required')
241
+ end
242
+ end
243
+ ```
244
+
245
+ For API-key failures, the JSON response uses `"Authentication Required"` in
246
+ `error` and places the specific reason, such as `"Invalid API key"`, in
247
+ `message`.
248
+
249
+ Use a role-aware strategy when testing `role=`. The built-in `SessionStrategy`
250
+ exposes the user ID but does not copy roles from `rack.session`; `role=` checks
251
+ the successful strategy result, not the session independently. See
252
+ [`spec/otto/security/authentication/route_auth_wrapper/role_authorization_spec.rb`](../../spec/otto/security/authentication/route_auth_wrapper/role_authorization_spec.rb)
253
+ for supported user shapes.
254
+
255
+ ## Test JSON request parsing through a handler
256
+
257
+ Do not manually merge JSON and query hashes when the behavior under test is
258
+ Otto's parser. Send an `application/json` body through a real Otto instance or
259
+ `LogicClassHandler`, then assert on the parameters received by the Logic object.
260
+
261
+ Current behavior to cover explicitly:
262
+
263
+ - a JSON object is merged into the Logic parameters;
264
+ - a valid non-object JSON value is ignored;
265
+ - malformed JSON is logged and the Logic class continues with other parameters;
266
+ - non-JSON bodies are not parsed by the Logic handler.
267
+
268
+ The maintained executable examples are in the “JSON request body parsing”
269
+ context of
270
+ [`spec/otto/route_handlers_spec.rb`](../../spec/otto/route_handlers_spec.rb).
271
+
272
+ ## Test configuration freezing explicitly
273
+
274
+ `Otto#call` does not automatically freeze configuration while `RSpec` is
275
+ defined. Freeze the instance directly in tests that assert the production boot
276
+ boundary:
277
+
278
+ ```ruby
279
+ RSpec.describe 'configuration freezing' do
280
+ it 'rejects later strategy registration' do
281
+ otto = build_otto(['GET / &health'], lambda_handlers: {
282
+ health: ->(_req, res, _path_params) { res.body = 'ok' },
283
+ })
284
+ otto.freeze_configuration!
285
+
286
+ expect(otto.frozen_configuration?).to be(true)
287
+ expect {
288
+ otto.add_auth_strategy('other', Object.new)
289
+ }.to raise_error(FrozenError, /Cannot modify frozen configuration/)
290
+ end
291
+ end
292
+ ```
293
+
294
+ Use a fresh Otto instance after an explicit freeze. `Otto.unfreeze_for_testing`
295
+ only resets an internal flag; it cannot unfreeze nested Ruby objects. See the
296
+ [configuration-freezing guide](configuration_freezing.md) and
297
+ [`spec/otto/configuration_freezing_spec.rb`](../../spec/otto/configuration_freezing_spec.rb).
298
+
299
+ ## Test CSRF at the correct layers
300
+
301
+ CSRF has two distinct components:
302
+
303
+ - `Otto::Security::Middleware::CSRFMiddleware` injects generated tokens into
304
+ HTML responses containing a `<head>` element.
305
+ - `Otto::Security::CSRFEnforcementWrapper` validates unsafe requests after route
306
+ matching, where it can honor `csrf=exempt`.
307
+
308
+ Enable CSRF on an `Otto::Security::Config` before testing either component.
309
+ Valid request tokens must come from `config.generate_csrf_token(session_id)` and
310
+ must use the matching session ID; an arbitrary value copied into the session and
311
+ header is not a valid token.
312
+
313
+ Use these maintained specs as executable examples:
314
+
315
+ - [`spec/security_csrf_spec.rb`](../../spec/security_csrf_spec.rb) — response injection.
316
+ - [`spec/otto/security/csrf_enforcement_wrapper_spec.rb`](../../spec/otto/security/csrf_enforcement_wrapper_spec.rb) — safe methods, unsafe methods, valid tokens, and `csrf=exempt`.
317
+ - [`spec/otto/security/csrf_validation_spec.rb`](../../spec/otto/security/csrf_validation_spec.rb) — token and session extraction.
318
+
319
+ ## Test IP privacy with the application configuration
320
+
321
+ The middleware class is
322
+ `Otto::Security::Middleware::IPPrivacyMiddleware`. It receives a security
323
+ configuration object, not `masking_level:` or `skip_private_ips:` keywords:
324
+
325
+ ```ruby
326
+ RSpec.describe Otto::Security::Middleware::IPPrivacyMiddleware do
327
+ it 'masks a public IPv4 address' do
328
+ inner = ->(_env) { [200, {}, ['ok']] }
329
+ security_config = Otto::Security::Config.new
330
+ middleware = described_class.new(inner, security_config)
331
+ env = rack_env('/')
332
+ env['REMOTE_ADDR'] = '203.0.113.45'
333
+
334
+ middleware.call(env)
335
+
336
+ expect(env['REMOTE_ADDR']).to eq('203.0.113.0')
337
+ end
338
+ end
339
+ ```
340
+
341
+ Set `security_config.ip_privacy_config.octet_precision = 2` to mask two IPv4
342
+ octets, or set `mask_private_ips = true` to include private and localhost
343
+ addresses. For application-facing tests, prefer a real Otto instance configured
344
+ through `configure_ip_privacy`.
345
+
346
+ See the [privacy guide](privacy.md) and the maintained privacy specs:
347
+
348
+ - [`spec/otto/ip_privacy_spec.rb`](../../spec/otto/ip_privacy_spec.rb)
349
+ - [`spec/otto/ip_precision_capability_spec.rb`](../../spec/otto/ip_precision_capability_spec.rb)
350
+
351
+ ## Test expected errors through the public request path
352
+
353
+ Register expected business errors before the freeze boundary, trigger them from
354
+ a route, and assert the returned status and response format. This verifies route
355
+ content negotiation and centralized error handling together. Direct calls to
356
+ private methods such as `handle_error` are useful for Otto's own unit tests but
357
+ should not be the main application-level pattern.
358
+
359
+ The maintained coverage is in:
360
+
361
+ - [`spec/otto/error_handler_registration_spec.rb`](../../spec/otto/error_handler_registration_spec.rb)
362
+ - [`spec/otto/error_handling_spec.rb`](../../spec/otto/error_handling_spec.rb)
363
+
364
+ ## Security-header expectations
365
+
366
+ Otto's default route responses include:
367
+
368
+ - `x-content-type-options: nosniff`
369
+ - `x-xss-protection: 1; mode=block`
370
+ - `referrer-policy: strict-origin-when-cross-origin`
371
+
372
+ `x-frame-options` is not a default header. Call
373
+ `otto.enable_frame_protection!` before the first request if a test should expect
374
+ `x-frame-options: SAMEORIGIN`.
375
+
376
+ Test security behavior at the narrowest useful level, but do not require every
377
+ unrelated unit test to repeat header and privacy assertions. Keep those checks in
378
+ focused middleware or request specs.
379
+
380
+ ## Maintained examples by task
381
+
382
+ - Authentication responses and route roles:
383
+ [`spec/otto/security/route_auth_wrapper_spec.rb`](../../spec/otto/security/route_auth_wrapper_spec.rb)
384
+ - Terminal API-key behavior:
385
+ [`spec/otto/security/authentication/api_key_fail_closed_integration_spec.rb`](../../spec/otto/security/authentication/api_key_fail_closed_integration_spec.rb)
386
+ - Registered lambda routes and response types:
387
+ [`spec/otto/lambda_routes_integration_spec.rb`](../../spec/otto/lambda_routes_integration_spec.rb)
388
+ - Response selection:
389
+ [`spec/otto/response_integration_spec.rb`](../../spec/otto/response_integration_spec.rb)
390
+ - Static files after freezing:
391
+ [`spec/otto/static_file_freezing_spec.rb`](../../spec/otto/static_file_freezing_spec.rb)
@@ -0,0 +1,41 @@
1
+ # GitHub Actions
2
+
3
+ ## Every `uses:` is pinned to a commit SHA
4
+
5
+ All third-party actions in `.github/workflows/` are pinned to a full 40-character
6
+ commit SHA with a trailing version comment:
7
+
8
+ ```yaml
9
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
10
+ ```
11
+
12
+ Tags and branches are mutable — an upstream force-push silently changes what runs
13
+ with our secrets. The SHA is the security boundary; the comment is only for humans.
14
+ Do not merge a workflow that references `@v4`, `@main`, or `@beta`.
15
+
16
+ Keep one version per action across the repo so the pins converge.
17
+
18
+ ## Resolving a tag to a SHA
19
+
20
+ ```sh
21
+ gh api repos/actions/checkout/commits/v7.0.1 --jq .sha
22
+ ```
23
+
24
+ This dereferences annotated tags for you. To see what tags exist:
25
+ `gh api repos/OWNER/REPO/releases --jq '.[].tag_name' | head`.
26
+
27
+ ## Updating pins
28
+
29
+ Dependabot (`.github/dependabot.yml`) checks weekly and rewrites both the SHA and
30
+ the version comment. The `github-actions` group means all bumps arrive in a single
31
+ PR rather than one per action. Review the upstream changelog before merging;
32
+ a SHA bump is arbitrary code execution in CI.
33
+
34
+ ## claude-code-action
35
+
36
+ Pinned to `v1.0.216`. `@beta` is a branch, not a release, so it is not permitted
37
+ here. The v1 line dropped several v0 inputs — `model`, `fallback_model`,
38
+ `direct_prompt` — which now go through `claude_args` (CLI flags) and `prompt`.
39
+ When bumping, diff the new tag's `action.yml` `inputs:` against the `with:` keys
40
+ in `claude.yml` and `claude-code-review.yml`; undeclared inputs are ignored
41
+ silently rather than failing the run.
@@ -0,0 +1,2 @@
1
+ *
2
+ !.gitignore