rubino-agent 0.3.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 +7 -0
- data/.rspec +3 -0
- data/.rubocop.yml +115 -0
- data/.rubocop_todo.yml +955 -0
- data/.ruby-version +1 -0
- data/AGENTS.md +97 -0
- data/CHANGELOG.md +344 -0
- data/CONTRIBUTING.md +69 -0
- data/LICENSE +21 -0
- data/README.md +200 -0
- data/Rakefile +8 -0
- data/docs/agents.md +190 -0
- data/docs/api/v1.md +414 -0
- data/docs/architecture.md +177 -0
- data/docs/commands.md +375 -0
- data/docs/configuration.md +590 -0
- data/docs/getting-started.md +143 -0
- data/docs/jobs.md +332 -0
- data/docs/mcp.md +128 -0
- data/docs/memory.md +98 -0
- data/docs/models-and-keys.md +173 -0
- data/docs/oauth-providers.md +145 -0
- data/docs/plugins.md +195 -0
- data/docs/security.md +145 -0
- data/docs/skills.md +322 -0
- data/docs/tools.md +395 -0
- data/docs/troubleshooting.md +73 -0
- data/exe/rubino +9 -0
- data/install.sh +275 -0
- data/lib/rubino/active_skill.rb +50 -0
- data/lib/rubino/agent/agent_registry.rb +120 -0
- data/lib/rubino/agent/backoff_policy.rb +116 -0
- data/lib/rubino/agent/definition.rb +128 -0
- data/lib/rubino/agent/degenerate_recovery.rb +271 -0
- data/lib/rubino/agent/fallback_chain.rb +194 -0
- data/lib/rubino/agent/iteration_budget.rb +50 -0
- data/lib/rubino/agent/loop.rb +617 -0
- data/lib/rubino/agent/model_call_runner.rb +383 -0
- data/lib/rubino/agent/prompts/build.txt +69 -0
- data/lib/rubino/agent/prompts/compaction.txt +20 -0
- data/lib/rubino/agent/prompts/explore.txt +19 -0
- data/lib/rubino/agent/prompts/general.txt +20 -0
- data/lib/rubino/agent/prompts/plan.txt +31 -0
- data/lib/rubino/agent/response_validator.rb +70 -0
- data/lib/rubino/agent/router.rb +65 -0
- data/lib/rubino/agent/runner.rb +195 -0
- data/lib/rubino/agent/tool_executor.rb +402 -0
- data/lib/rubino/agent/truncation_continuation.rb +137 -0
- data/lib/rubino/api/middleware/auth.rb +43 -0
- data/lib/rubino/api/middleware/error_handler.rb +65 -0
- data/lib/rubino/api/middleware/json_parser.rb +100 -0
- data/lib/rubino/api/middleware/observability.rb +59 -0
- data/lib/rubino/api/middleware/rate_limit.rb +136 -0
- data/lib/rubino/api/operations/approvals/decide_operation.rb +49 -0
- data/lib/rubino/api/operations/clarifications/decide_operation.rb +44 -0
- data/lib/rubino/api/operations/cron_jobs/create_operation.rb +46 -0
- data/lib/rubino/api/operations/cron_jobs/delete_operation.rb +36 -0
- data/lib/rubino/api/operations/cron_jobs/list_operation.rb +55 -0
- data/lib/rubino/api/operations/cron_jobs/pause_operation.rb +34 -0
- data/lib/rubino/api/operations/cron_jobs/resume_operation.rb +34 -0
- data/lib/rubino/api/operations/cron_jobs/schedule_validation.rb +30 -0
- data/lib/rubino/api/operations/cron_jobs/show_operation.rb +32 -0
- data/lib/rubino/api/operations/cron_jobs/trigger_operation.rb +38 -0
- data/lib/rubino/api/operations/cron_jobs/update_operation.rb +42 -0
- data/lib/rubino/api/operations/files/read_operation.rb +40 -0
- data/lib/rubino/api/operations/files/upload_operation.rb +175 -0
- data/lib/rubino/api/operations/health_operation.rb +46 -0
- data/lib/rubino/api/operations/memory/delete_operation.rb +32 -0
- data/lib/rubino/api/operations/memory/index_operation.rb +80 -0
- data/lib/rubino/api/operations/memory/stats_operation.rb +28 -0
- data/lib/rubino/api/operations/metrics_operation.rb +18 -0
- data/lib/rubino/api/operations/mode/show_operation.rb +29 -0
- data/lib/rubino/api/operations/mode/update_operation.rb +42 -0
- data/lib/rubino/api/operations/models/list_operation.rb +45 -0
- data/lib/rubino/api/operations/oauth/connections/disconnect_operation.rb +77 -0
- data/lib/rubino/api/operations/oauth/connections/list_operation.rb +36 -0
- data/lib/rubino/api/operations/oauth/providers/callback_operation.rb +82 -0
- data/lib/rubino/api/operations/oauth/providers/connect_operation.rb +44 -0
- data/lib/rubino/api/operations/oauth/providers/list_operation.rb +35 -0
- data/lib/rubino/api/operations/oauth/serializer.rb +21 -0
- data/lib/rubino/api/operations/runs/create_operation.rb +77 -0
- data/lib/rubino/api/operations/runs/events_operation.rb +195 -0
- data/lib/rubino/api/operations/runs/stop_operation.rb +34 -0
- data/lib/rubino/api/operations/sessions/create_operation.rb +46 -0
- data/lib/rubino/api/operations/sessions/delete_operation.rb +33 -0
- data/lib/rubino/api/operations/sessions/index_operation.rb +82 -0
- data/lib/rubino/api/operations/sessions/retry_operation.rb +45 -0
- data/lib/rubino/api/operations/sessions/show_operation.rb +59 -0
- data/lib/rubino/api/operations/sessions/undo_operation.rb +38 -0
- data/lib/rubino/api/operations/skills/list_operation.rb +34 -0
- data/lib/rubino/api/operations/skills/toggle_operation.rb +40 -0
- data/lib/rubino/api/operations/tasks/index_operation.rb +30 -0
- data/lib/rubino/api/operations/tasks/serializer.rb +60 -0
- data/lib/rubino/api/operations/tasks/show_operation.rb +33 -0
- data/lib/rubino/api/operations/tasks/stop_operation.rb +47 -0
- data/lib/rubino/api/request.rb +54 -0
- data/lib/rubino/api/responses.rb +64 -0
- data/lib/rubino/api/router.rb +72 -0
- data/lib/rubino/api/schemas.rb +103 -0
- data/lib/rubino/api/server.rb +102 -0
- data/lib/rubino/api/tls.rb +108 -0
- data/lib/rubino/attachments/classification.rb +16 -0
- data/lib/rubino/attachments/classify.rb +171 -0
- data/lib/rubino/attachments/defang.rb +47 -0
- data/lib/rubino/attachments/policy.rb +36 -0
- data/lib/rubino/attachments/preamble.rb +120 -0
- data/lib/rubino/boot/encryption_key.rb +32 -0
- data/lib/rubino/cli/chat/bang_shell.rb +257 -0
- data/lib/rubino/cli/chat/completion_builder.rb +290 -0
- data/lib/rubino/cli/chat/idle_card_host.rb +69 -0
- data/lib/rubino/cli/chat/image_inbox.rb +168 -0
- data/lib/rubino/cli/chat/session_resolver.rb +176 -0
- data/lib/rubino/cli/chat_command.rb +1674 -0
- data/lib/rubino/cli/commands.rb +250 -0
- data/lib/rubino/cli/config_command.rb +96 -0
- data/lib/rubino/cli/doctor_command.rb +251 -0
- data/lib/rubino/cli/jobs_command.rb +60 -0
- data/lib/rubino/cli/memory_command.rb +135 -0
- data/lib/rubino/cli/onboarding_wizard.rb +207 -0
- data/lib/rubino/cli/server_command.rb +139 -0
- data/lib/rubino/cli/session_command.rb +125 -0
- data/lib/rubino/cli/setup_command.rb +107 -0
- data/lib/rubino/cli/skills_command.rb +85 -0
- data/lib/rubino/cli/tools_command.rb +81 -0
- data/lib/rubino/cli/trust_gate.rb +71 -0
- data/lib/rubino/commands/built_ins.rb +46 -0
- data/lib/rubino/commands/command.rb +116 -0
- data/lib/rubino/commands/executor.rb +550 -0
- data/lib/rubino/commands/handlers/agents.rb +510 -0
- data/lib/rubino/commands/handlers/config.rb +88 -0
- data/lib/rubino/commands/handlers/help.rb +148 -0
- data/lib/rubino/commands/handlers/jobs.rb +71 -0
- data/lib/rubino/commands/handlers/mcp.rb +229 -0
- data/lib/rubino/commands/handlers/memory.rb +200 -0
- data/lib/rubino/commands/handlers/sessions.rb +207 -0
- data/lib/rubino/commands/handlers/skills.rb +195 -0
- data/lib/rubino/commands/handlers/status.rb +211 -0
- data/lib/rubino/commands/loader.rb +90 -0
- data/lib/rubino/config/configuration.rb +455 -0
- data/lib/rubino/config/defaults.rb +569 -0
- data/lib/rubino/config/loader.rb +115 -0
- data/lib/rubino/config/reasoning_prefs.rb +67 -0
- data/lib/rubino/config/writer.rb +72 -0
- data/lib/rubino/context/compressor.rb +149 -0
- data/lib/rubino/context/environment_inspector.rb +176 -0
- data/lib/rubino/context/file_discovery.rb +45 -0
- data/lib/rubino/context/message_boundary.rb +39 -0
- data/lib/rubino/context/prompt_assembler.rb +382 -0
- data/lib/rubino/context/summary_builder.rb +159 -0
- data/lib/rubino/context/token_budget.rb +68 -0
- data/lib/rubino/context/tool_pair_sanitizer.rb +70 -0
- data/lib/rubino/database/connection.rb +77 -0
- data/lib/rubino/database/migrations/001_create_initial_schema.rb +156 -0
- data/lib/rubino/database/migrations/002_create_runs.rb +45 -0
- data/lib/rubino/database/migrations/003_create_skill_states.rb +15 -0
- data/lib/rubino/database/migrations/004_create_cron_jobs.rb +36 -0
- data/lib/rubino/database/migrations/005_create_oauth_connections.rb +27 -0
- data/lib/rubino/database/migrations/006_create_webhook_deliveries.rb +34 -0
- data/lib/rubino/database/migrations/007_create_messages_fts.rb +59 -0
- data/lib/rubino/database/migrations/008_create_memory_facts.rb +75 -0
- data/lib/rubino/database/migrations/009_create_memory_graph.rb +55 -0
- data/lib/rubino/database/migrations/010_add_owner_pid_to_sessions.rb +20 -0
- data/lib/rubino/database/migrator.rb +48 -0
- data/lib/rubino/documents/converters/csv.rb +79 -0
- data/lib/rubino/documents/converters/docx.rb +129 -0
- data/lib/rubino/documents/converters/html.rb +28 -0
- data/lib/rubino/documents/converters/json.rb +35 -0
- data/lib/rubino/documents/converters/pdf.rb +59 -0
- data/lib/rubino/documents/converters/plain.rb +68 -0
- data/lib/rubino/documents/converters/pptx.rb +64 -0
- data/lib/rubino/documents/converters/xlsx.rb +62 -0
- data/lib/rubino/documents/converters/xml.rb +45 -0
- data/lib/rubino/documents/html.rb +71 -0
- data/lib/rubino/documents/registry.rb +68 -0
- data/lib/rubino/documents/table.rb +63 -0
- data/lib/rubino/documents.rb +50 -0
- data/lib/rubino/errors.rb +119 -0
- data/lib/rubino/files/workspace.rb +93 -0
- data/lib/rubino/interaction/cancel_token.rb +43 -0
- data/lib/rubino/interaction/clipboard_image.rb +84 -0
- data/lib/rubino/interaction/event_bus.rb +48 -0
- data/lib/rubino/interaction/events.rb +101 -0
- data/lib/rubino/interaction/image_input.rb +127 -0
- data/lib/rubino/interaction/input_queue.rb +117 -0
- data/lib/rubino/interaction/lifecycle.rb +299 -0
- data/lib/rubino/interaction/probe.rb +65 -0
- data/lib/rubino/interaction/state.rb +56 -0
- data/lib/rubino/jobs/cron_job_repository.rb +75 -0
- data/lib/rubino/jobs/handlers/cleanup_sessions_job.rb +32 -0
- data/lib/rubino/jobs/handlers/compact_session_job.rb +21 -0
- data/lib/rubino/jobs/handlers/distill_skill_job.rb +186 -0
- data/lib/rubino/jobs/handlers/extract_memory_job.rb +37 -0
- data/lib/rubino/jobs/handlers/summarize_session_job.rb +21 -0
- data/lib/rubino/jobs/queue.rb +184 -0
- data/lib/rubino/jobs/registry.rb +45 -0
- data/lib/rubino/jobs/runner.rb +79 -0
- data/lib/rubino/jobs/scheduler.rb +138 -0
- data/lib/rubino/jobs/webhook_delivery.rb +225 -0
- data/lib/rubino/jobs/worker.rb +59 -0
- data/lib/rubino/llm/adapter_factory.rb +47 -0
- data/lib/rubino/llm/adapter_response.rb +65 -0
- data/lib/rubino/llm/auxiliary_client.rb +61 -0
- data/lib/rubino/llm/bedrock_bearer_client.rb +235 -0
- data/lib/rubino/llm/content_builder.rb +55 -0
- data/lib/rubino/llm/credential_check.rb +93 -0
- data/lib/rubino/llm/error_classifier.rb +364 -0
- data/lib/rubino/llm/fake_provider.rb +292 -0
- data/lib/rubino/llm/inline_think_filter.rb +58 -0
- data/lib/rubino/llm/model_catalog.rb +29 -0
- data/lib/rubino/llm/provider_resolver.rb +48 -0
- data/lib/rubino/llm/reasoning_manager.rb +100 -0
- data/lib/rubino/llm/request.rb +56 -0
- data/lib/rubino/llm/ruby_llm_adapter.rb +794 -0
- data/lib/rubino/llm/scenario_loader.rb +68 -0
- data/lib/rubino/llm/scenario_selector.rb +80 -0
- data/lib/rubino/llm/scenarios/agent-creates-cron-failure.yml +29 -0
- data/lib/rubino/llm/scenarios/agent-creates-cron.yml +36 -0
- data/lib/rubino/llm/scenarios/analysis.yml +501 -0
- data/lib/rubino/llm/scenarios/complex-analysis.yml +598 -0
- data/lib/rubino/llm/scenarios/failure.yml +65 -0
- data/lib/rubino/llm/scenarios/happy-path.yml +24 -0
- data/lib/rubino/llm/scenarios/provider-quota-completed.yml +14 -0
- data/lib/rubino/llm/scenarios/wide-table.yml +121 -0
- data/lib/rubino/llm/scenarios/with-approvals.yml +50 -0
- data/lib/rubino/llm/scenarios/with-artifacts.yml +98 -0
- data/lib/rubino/llm/scenarios/with-clarify.yml +32 -0
- data/lib/rubino/llm/scenarios/with-reasoning.yml +175 -0
- data/lib/rubino/llm/scenarios/with-uploads.yml +104 -0
- data/lib/rubino/llm/thinking_support.rb +84 -0
- data/lib/rubino/llm/tool_bridge.rb +89 -0
- data/lib/rubino/logger.rb +99 -0
- data/lib/rubino/mcp/manager.rb +180 -0
- data/lib/rubino/mcp/mcp_tool_wrapper.rb +69 -0
- data/lib/rubino/mcp.rb +57 -0
- data/lib/rubino/memory/backend.rb +104 -0
- data/lib/rubino/memory/backends/default.rb +101 -0
- data/lib/rubino/memory/backends/sqlite.rb +653 -0
- data/lib/rubino/memory/backends.rb +53 -0
- data/lib/rubino/memory/deduplicator.rb +74 -0
- data/lib/rubino/memory/extractor.rb +85 -0
- data/lib/rubino/memory/flusher.rb +31 -0
- data/lib/rubino/memory/retriever.rb +50 -0
- data/lib/rubino/memory/sqlite_extraction_prompt.rb +70 -0
- data/lib/rubino/memory/sqlite_graph.rb +154 -0
- data/lib/rubino/memory/store.rb +228 -0
- data/lib/rubino/memory/threat_scanner.rb +68 -0
- data/lib/rubino/metrics.rb +175 -0
- data/lib/rubino/modes.rb +93 -0
- data/lib/rubino/oauth/connection_repository.rb +95 -0
- data/lib/rubino/oauth/provider/github.rb +75 -0
- data/lib/rubino/oauth/provider/google.rb +59 -0
- data/lib/rubino/oauth/provider.rb +149 -0
- data/lib/rubino/oauth/registry.rb +86 -0
- data/lib/rubino/oauth/token_encryptor.rb +87 -0
- data/lib/rubino/plugins/registry.rb +75 -0
- data/lib/rubino/plugins.rb +86 -0
- data/lib/rubino/run/approval_gate.rb +243 -0
- data/lib/rubino/run/attachment_downloader.rb +166 -0
- data/lib/rubino/run/event_store.rb +74 -0
- data/lib/rubino/run/executor.rb +383 -0
- data/lib/rubino/run/gate_registry.rb +39 -0
- data/lib/rubino/run/recorder.rb +69 -0
- data/lib/rubino/run/repository.rb +118 -0
- data/lib/rubino/run/session_approval_cache.rb +118 -0
- data/lib/rubino/security/allowlist_persister.rb +55 -0
- data/lib/rubino/security/approval_policy.rb +227 -0
- data/lib/rubino/security/command_allowlist.rb +24 -0
- data/lib/rubino/security/dangerous_patterns.rb +118 -0
- data/lib/rubino/security/deny_persister.rb +73 -0
- data/lib/rubino/security/doom_loop_detector.rb +43 -0
- data/lib/rubino/security/hardline_guard.rb +105 -0
- data/lib/rubino/security/pattern_matcher.rb +62 -0
- data/lib/rubino/security/prefix_deriver.rb +124 -0
- data/lib/rubino/security/readonly_commands.rb +211 -0
- data/lib/rubino/session/exporter.rb +101 -0
- data/lib/rubino/session/message.rb +77 -0
- data/lib/rubino/session/repository.rb +295 -0
- data/lib/rubino/session/store.rb +198 -0
- data/lib/rubino/session/summary_store.rb +65 -0
- data/lib/rubino/skills/prompt_index.rb +85 -0
- data/lib/rubino/skills/registry.rb +208 -0
- data/lib/rubino/skills/skill.rb +176 -0
- data/lib/rubino/skills/skill_tool.rb +215 -0
- data/lib/rubino/skills/state_repository.rb +37 -0
- data/lib/rubino/skills/toggle.rb +26 -0
- data/lib/rubino/tools/answer_child_tool.rb +83 -0
- data/lib/rubino/tools/ask_parent_tool.rb +232 -0
- data/lib/rubino/tools/attach_file_tool.rb +120 -0
- data/lib/rubino/tools/background_tasks.rb +520 -0
- data/lib/rubino/tools/base.rb +222 -0
- data/lib/rubino/tools/custom_tool_loader.rb +119 -0
- data/lib/rubino/tools/edit_tool.rb +122 -0
- data/lib/rubino/tools/git_tool.rb +71 -0
- data/lib/rubino/tools/github_tool.rb +233 -0
- data/lib/rubino/tools/glob_tool.rb +69 -0
- data/lib/rubino/tools/grep_tool.rb +206 -0
- data/lib/rubino/tools/memory_tool.rb +184 -0
- data/lib/rubino/tools/multi_edit_tool.rb +110 -0
- data/lib/rubino/tools/patch_tool.rb +260 -0
- data/lib/rubino/tools/probe_tool.rb +175 -0
- data/lib/rubino/tools/question_tool.rb +128 -0
- data/lib/rubino/tools/read_attachment_tool.rb +180 -0
- data/lib/rubino/tools/read_tool.rb +212 -0
- data/lib/rubino/tools/read_tracker.rb +98 -0
- data/lib/rubino/tools/registry.rb +166 -0
- data/lib/rubino/tools/result.rb +113 -0
- data/lib/rubino/tools/ruby_tool.rb +0 -0
- data/lib/rubino/tools/session_search_tool.rb +103 -0
- data/lib/rubino/tools/shell_input_tool.rb +96 -0
- data/lib/rubino/tools/shell_kill_tool.rb +76 -0
- data/lib/rubino/tools/shell_output_tool.rb +72 -0
- data/lib/rubino/tools/shell_registry.rb +158 -0
- data/lib/rubino/tools/shell_tail_tool.rb +118 -0
- data/lib/rubino/tools/shell_tool.rb +330 -0
- data/lib/rubino/tools/steer_tool.rb +118 -0
- data/lib/rubino/tools/subagent_probe.rb +89 -0
- data/lib/rubino/tools/summarize_file_tool.rb +182 -0
- data/lib/rubino/tools/task_result_tool.rb +90 -0
- data/lib/rubino/tools/task_stop_tool.rb +80 -0
- data/lib/rubino/tools/task_tool.rb +622 -0
- data/lib/rubino/tools/test_tool.rb +454 -0
- data/lib/rubino/tools/todo_tool.rb +93 -0
- data/lib/rubino/tools/tool_call_repository.rb +33 -0
- data/lib/rubino/tools/vision_tool.rb +85 -0
- data/lib/rubino/tools/webfetch_tool.rb +153 -0
- data/lib/rubino/tools/websearch_tool.rb +179 -0
- data/lib/rubino/tools/write_tool.rb +61 -0
- data/lib/rubino/trust.rb +88 -0
- data/lib/rubino/ui/api.rb +296 -0
- data/lib/rubino/ui/base.rb +252 -0
- data/lib/rubino/ui/bottom_composer.rb +1599 -0
- data/lib/rubino/ui/cli.rb +1987 -0
- data/lib/rubino/ui/completion_menu.rb +321 -0
- data/lib/rubino/ui/completion_source.rb +284 -0
- data/lib/rubino/ui/escape_reader.rb +169 -0
- data/lib/rubino/ui/indented_io.rb +88 -0
- data/lib/rubino/ui/input_history.rb +108 -0
- data/lib/rubino/ui/live_region.rb +183 -0
- data/lib/rubino/ui/markdown_renderer.rb +506 -0
- data/lib/rubino/ui/notifier.rb +163 -0
- data/lib/rubino/ui/null.rb +195 -0
- data/lib/rubino/ui/paste_store.rb +176 -0
- data/lib/rubino/ui/printer_base.rb +79 -0
- data/lib/rubino/ui/probe_wait_indicator.rb +75 -0
- data/lib/rubino/ui/queued_indicators.rb +66 -0
- data/lib/rubino/ui/status_bar.rb +100 -0
- data/lib/rubino/ui/stdout_proxy.rb +161 -0
- data/lib/rubino/ui/streaming_markdown.rb +186 -0
- data/lib/rubino/ui/subagent_cards.rb +134 -0
- data/lib/rubino/ui/subagent_view.rb +255 -0
- data/lib/rubino/ui.rb +21 -0
- data/lib/rubino/update_check.rb +187 -0
- data/lib/rubino/util/duration.rb +23 -0
- data/lib/rubino/util/hyperlink.rb +105 -0
- data/lib/rubino/util/output.rb +145 -0
- data/lib/rubino/util/secrets_mask.rb +83 -0
- data/lib/rubino/version.rb +5 -0
- data/lib/rubino/workspace.rb +85 -0
- data/lib/rubino-agent.rb +5 -0
- data/lib/rubino.rb +318 -0
- data/mise.toml +2 -0
- data/rubino-agent.gemspec +103 -0
- data/skills/ruby-expert/SKILL.md +67 -0
- data/skills/ruby-expert/references/concurrency.md +357 -0
- data/skills/ruby-expert/references/datetime-and-encoding.md +363 -0
- data/skills/ruby-expert/references/errors-and-types.md +460 -0
- data/skills/ruby-expert/references/gem-authoring.md +459 -0
- data/skills/ruby-expert/references/language-idioms.md +465 -0
- data/skills/ruby-expert/references/metaprogramming.md +339 -0
- data/skills/ruby-expert/references/oo-design.md +553 -0
- data/skills/ruby-expert/references/performance.md +383 -0
- data/skills/ruby-expert/references/rails.md +424 -0
- data/skills/ruby-expert/references/security.md +404 -0
- data/skills/ruby-expert/references/testing.md +473 -0
- data/skills/ruby-expert/references/tooling.md +466 -0
- metadata +856 -0
|
@@ -0,0 +1,459 @@
|
|
|
1
|
+
# Authoring & publishing gems
|
|
2
|
+
|
|
3
|
+
Building, structuring, testing and releasing a Ruby gem (Ruby 3.2–3.4, modern RubyGems/Bundler). For app dependency management with Bundler see `references/tooling.md`; for class/module design see `references/oo-design.md`.
|
|
4
|
+
|
|
5
|
+
## Scaffold a gem
|
|
6
|
+
|
|
7
|
+
Use Bundler's generator — never hand-roll the layout.
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
bundle gem rubino --test=rspec --ci=github --linter=rubocop
|
|
11
|
+
# flags: --exe (CLI binstub), --coc (code of conduct), --mit (license)
|
|
12
|
+
# --changelog (Keep a Changelog stub)
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
This produces the canonical skeleton:
|
|
16
|
+
|
|
17
|
+
```
|
|
18
|
+
rubino/
|
|
19
|
+
├── rubino.gemspec
|
|
20
|
+
├── Gemfile # only: gemspec + dev-only tools
|
|
21
|
+
├── Rakefile
|
|
22
|
+
├── README.md
|
|
23
|
+
├── CHANGELOG.md
|
|
24
|
+
├── LICENSE.txt
|
|
25
|
+
├── .rubocop.yml
|
|
26
|
+
├── .github/workflows/main.yml
|
|
27
|
+
├── bin/ # dev helpers: console, setup (NOT shipped)
|
|
28
|
+
├── exe/ # user-facing executables (shipped, on PATH)
|
|
29
|
+
├── sig/rubino.rbs # RBS signatures (see errors-and-types.md)
|
|
30
|
+
├── lib/
|
|
31
|
+
│ ├── rubino.rb # entrypoint: requires version + sets up loader
|
|
32
|
+
│ └── rubino/
|
|
33
|
+
│ └── version.rb # Rubino::VERSION = "0.1.0"
|
|
34
|
+
└── spec/
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
**Gemfile vs gemspec.** Runtime + development *gem* dependencies belong in the `.gemspec`. The `Gemfile` is one line — `gemspec` — plus dev-only tools you don't want as formal dev dependencies. Don't duplicate dependency lists between them.
|
|
38
|
+
|
|
39
|
+
```ruby
|
|
40
|
+
# Gemfile
|
|
41
|
+
source "https://rubygems.org"
|
|
42
|
+
gemspec
|
|
43
|
+
gem "rake", "~> 13.0" # tooling-only, fine to keep out of gemspec
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## The entrypoint and version file
|
|
47
|
+
|
|
48
|
+
`version.rb` holds *only* the version constant so tooling (and `rake release`) can read it without loading the whole library.
|
|
49
|
+
|
|
50
|
+
```ruby
|
|
51
|
+
# lib/rubino/version.rb
|
|
52
|
+
module Rubino
|
|
53
|
+
VERSION = "0.1.0"
|
|
54
|
+
end
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
```ruby
|
|
58
|
+
# lib/rubino.rb
|
|
59
|
+
# frozen_string_literal: true
|
|
60
|
+
|
|
61
|
+
require_relative "rubino/version"
|
|
62
|
+
require "zeitwerk"
|
|
63
|
+
|
|
64
|
+
module Rubino
|
|
65
|
+
class Error < StandardError; end # library base error (see errors-and-types.md)
|
|
66
|
+
|
|
67
|
+
Loader = Zeitwerk::Loader.for_gem
|
|
68
|
+
Loader.setup
|
|
69
|
+
end
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
`Zeitwerk::Loader.for_gem` configures the loader to manage `lib/`, automatically ignoring `lib/rubino.rb` itself and `lib/rubino/version.rb` (already required). Do not `require` your own source files after this — Zeitwerk autoloads them on first constant reference.
|
|
73
|
+
|
|
74
|
+
## The gemspec
|
|
75
|
+
|
|
76
|
+
```ruby
|
|
77
|
+
# rubino.gemspec
|
|
78
|
+
# frozen_string_literal: true
|
|
79
|
+
|
|
80
|
+
require_relative "lib/rubino/version"
|
|
81
|
+
|
|
82
|
+
Gem::Specification.new do |spec|
|
|
83
|
+
spec.name = "rubino"
|
|
84
|
+
spec.version = Rubino::VERSION
|
|
85
|
+
spec.authors = ["Jane Dev"]
|
|
86
|
+
spec.email = ["jane@example.com"]
|
|
87
|
+
|
|
88
|
+
spec.summary = "Short one-line description (< ~100 chars, no trailing period)."
|
|
89
|
+
spec.description = "A longer paragraph describing what the gem does and why."
|
|
90
|
+
spec.homepage = "https://github.com/acme/rubino"
|
|
91
|
+
spec.license = "MIT"
|
|
92
|
+
spec.required_ruby_version = ">= 3.2.0"
|
|
93
|
+
|
|
94
|
+
# Metadata powers rubygems.org links + enables MFA-protected pushes.
|
|
95
|
+
spec.metadata["homepage_uri"] = spec.homepage
|
|
96
|
+
spec.metadata["source_code_uri"] = "https://github.com/acme/rubino"
|
|
97
|
+
spec.metadata["changelog_uri"] = "https://github.com/acme/rubino/blob/main/CHANGELOG.md"
|
|
98
|
+
spec.metadata["bug_tracker_uri"] = "https://github.com/acme/rubino/issues"
|
|
99
|
+
spec.metadata["documentation_uri"] = "https://rubydoc.info/gems/rubino"
|
|
100
|
+
spec.metadata["rubygems_mfa_required"] = "true"
|
|
101
|
+
|
|
102
|
+
# File list driven by git — never glob the whole dir (avoids shipping junk).
|
|
103
|
+
gemspec = File.basename(__FILE__)
|
|
104
|
+
spec.files = IO.popen(
|
|
105
|
+
%w[git ls-files -z], chdir: __dir__, err: IO::NULL
|
|
106
|
+
) do |ls|
|
|
107
|
+
ls.readlines("\x0", chomp: true).reject do |f|
|
|
108
|
+
(f == gemspec) ||
|
|
109
|
+
f.start_with?(*%w[bin/ test/ spec/ features/ .git .github appveyor Gemfile])
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
spec.bindir = "exe"
|
|
114
|
+
spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
|
|
115
|
+
spec.require_paths = ["lib"]
|
|
116
|
+
|
|
117
|
+
# Runtime deps: required for the gem to function.
|
|
118
|
+
spec.add_dependency "zeitwerk", "~> 2.6"
|
|
119
|
+
spec.add_dependency "thor", "~> 1.3"
|
|
120
|
+
|
|
121
|
+
# Dev deps: needed only to develop/test the gem.
|
|
122
|
+
spec.add_development_dependency "rspec", "~> 3.13"
|
|
123
|
+
end
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
Notes:
|
|
127
|
+
|
|
128
|
+
- **`git ls-files`** is the modern idiom — only tracked files ship. Untracked build artifacts, `.env`, `tmp/` never leak into the `.gem`.
|
|
129
|
+
- **`required_ruby_version`** gates installs on old Rubies with a clear error instead of a mysterious syntax failure.
|
|
130
|
+
- **`rubygems_mfa_required = "true"`** forces MFA for anyone pushing the gem — set it.
|
|
131
|
+
- Modern RubyGems: `add_dependency` *is* a runtime dependency. `add_runtime_dependency` is the old alias; either works, but don't pass a `:development` type to `add_dependency`.
|
|
132
|
+
|
|
133
|
+
### Dependency version policy
|
|
134
|
+
|
|
135
|
+
```ruby
|
|
136
|
+
spec.add_dependency "thor", "~> 1.3" # >= 1.3.0, < 2.0 (RIGHT — pessimistic)
|
|
137
|
+
spec.add_dependency "thor", ">= 1.3", "< 3" # explicit range when you support 2 majors
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
Do / don't:
|
|
141
|
+
|
|
142
|
+
- DO use the pessimistic `~>` operator to allow compatible upgrades while excluding the next breaking major.
|
|
143
|
+
- DON'T pin to an exact version (`"= 1.3.2"`) in a library — it causes unresolvable conflicts in apps that depend on you. Exact pins belong in `Gemfile.lock` (apps), not gemspecs.
|
|
144
|
+
- DON'T leave a dependency unbounded (`>= 0`) — a future major can break your users silently.
|
|
145
|
+
- Keep the *floor* honest: require the lowest version whose API you actually use.
|
|
146
|
+
|
|
147
|
+
## Autoloading with Zeitwerk
|
|
148
|
+
|
|
149
|
+
Zeitwerk maps file paths to constant names. Follow its conventions and you never write `require` again.
|
|
150
|
+
|
|
151
|
+
```
|
|
152
|
+
lib/rubino.rb -> (root file, loads the gem)
|
|
153
|
+
lib/rubino/client.rb -> Rubino::Client
|
|
154
|
+
lib/rubino/http_client.rb -> Rubino::HTTPClient (acronym, see below)
|
|
155
|
+
lib/rubino/cli/runner.rb -> Rubino::CLI::Runner
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
```ruby
|
|
159
|
+
# RIGHT: file name and constant agree
|
|
160
|
+
# lib/rubino/http_client.rb
|
|
161
|
+
module Rubino
|
|
162
|
+
class HTTPClient; end
|
|
163
|
+
end
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
Acronyms need an inflection rule, or Zeitwerk expects `Rubino::HttpClient`:
|
|
167
|
+
|
|
168
|
+
```ruby
|
|
169
|
+
Loader = Zeitwerk::Loader.for_gem
|
|
170
|
+
Loader.inflector.inflect("http_client" => "HTTPClient", "cli" => "CLI")
|
|
171
|
+
Loader.setup
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
**Lazy (default) vs eager loading.** Lazy autoloading loads each file on first reference — ideal for libraries (fast boot, only pay for what's used). Eager-load when a host process forks (e.g. Puma/Sidekiq) or in CI to surface load errors:
|
|
175
|
+
|
|
176
|
+
```ruby
|
|
177
|
+
Loader.setup
|
|
178
|
+
Loader.eager_load if ENV["RUBINO_EAGER_LOAD"] # or eager_load_force in tests
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
In CI, run `bin/rubocop` plus a tiny spec that calls `Rubino::Loader.eager_load` to catch naming mismatches before release.
|
|
182
|
+
|
|
183
|
+
## CLI gems: `exe/` vs `bin/`
|
|
184
|
+
|
|
185
|
+
- `bin/` holds **development** helpers (`bin/console`, `bin/setup`) that are *not* packaged.
|
|
186
|
+
- `exe/` holds **user-facing** executables that go on the user's PATH (`spec.bindir = "exe"`).
|
|
187
|
+
|
|
188
|
+
The executable file should be thin — parse nothing, delegate to a class.
|
|
189
|
+
|
|
190
|
+
```ruby
|
|
191
|
+
#!/usr/bin/env ruby
|
|
192
|
+
# frozen_string_literal: true
|
|
193
|
+
# exe/rubino (chmod +x)
|
|
194
|
+
|
|
195
|
+
require "rubino"
|
|
196
|
+
Rubino::CLI.start(ARGV)
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
Use **Thor** for anything beyond a single flag — it gives subcommands, options, and help for free.
|
|
200
|
+
|
|
201
|
+
```ruby
|
|
202
|
+
# lib/rubino/cli.rb
|
|
203
|
+
require "thor"
|
|
204
|
+
|
|
205
|
+
module Rubino
|
|
206
|
+
class CLI < Thor
|
|
207
|
+
def self.exit_on_failure? = true # exit 1 on errors, not raise
|
|
208
|
+
|
|
209
|
+
desc "build PATH", "Build the project at PATH"
|
|
210
|
+
option :force, type: :boolean, aliases: "-f", desc: "Overwrite existing output"
|
|
211
|
+
def build(path)
|
|
212
|
+
Builder.new(path, force: options[:force]).call
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
desc "version", "Print the version"
|
|
216
|
+
def version = say(Rubino::VERSION)
|
|
217
|
+
end
|
|
218
|
+
end
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
DON'T put business logic in the Thor class — it's hard to test and couples your domain to the CLI framework. Keep Thor as a thin adapter over plain objects (`Builder` above).
|
|
222
|
+
|
|
223
|
+
## Shipping non-code assets
|
|
224
|
+
|
|
225
|
+
Data files (templates, fixtures, certs, YAML) must be (a) tracked by git so `git ls-files` includes them, and (b) located at runtime relative to the file, never relative to CWD.
|
|
226
|
+
|
|
227
|
+
```ruby
|
|
228
|
+
# RIGHT: anchor on the gem's own directory
|
|
229
|
+
module Rubino
|
|
230
|
+
ROOT = File.expand_path("..", __dir__) # gem root from lib/rubino.rb
|
|
231
|
+
DATA_DIR = File.expand_path("templates", __dir__) # lib/rubino/templates
|
|
232
|
+
|
|
233
|
+
def self.template(name)
|
|
234
|
+
File.read(File.join(DATA_DIR, "#{name}.erb"))
|
|
235
|
+
end
|
|
236
|
+
end
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
```ruby
|
|
240
|
+
# WRONG: relative to the process working directory — breaks once installed
|
|
241
|
+
File.read("templates/default.erb") # NoMethodError-adjacent: file not found
|
|
242
|
+
File.read(Dir.pwd + "/lib/rubino/...") # depends on where the user ran the command
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
Use `__dir__` (the directory of the current file) over `File.dirname(__FILE__)` — same result, less noise. Place assets *under* `lib/` (e.g. `lib/rubino/templates/`) so Zeitwerk's path mapping isn't disturbed by them — Zeitwerk ignores non-`.rb` files automatically, but keeping them in a clearly-data subdir is cleanest. For larger data trees, `Loader.ignore("#{__dir__}/rubino/templates")` is explicit.
|
|
246
|
+
|
|
247
|
+
## Namespacing & avoiding constant pollution
|
|
248
|
+
|
|
249
|
+
Everything lives under your top-level module. One namespace, one top-level constant.
|
|
250
|
+
|
|
251
|
+
```ruby
|
|
252
|
+
# RIGHT
|
|
253
|
+
module Rubino
|
|
254
|
+
class Client; end
|
|
255
|
+
Config = Data.define(:timeout, :retries)
|
|
256
|
+
end
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
```ruby
|
|
260
|
+
# WRONG: leaks Client and Config into Object — collides with other gems
|
|
261
|
+
class Client; end
|
|
262
|
+
Config = Struct.new(:timeout)
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
Don't reopen/monkey-patch core classes from a gem; if you must extend, use **refinements** (lexically scoped) — see `references/metaprogramming.md`. Don't define top-level methods or constants.
|
|
266
|
+
|
|
267
|
+
## Semantic versioning & CHANGELOG
|
|
268
|
+
|
|
269
|
+
Follow [SemVer](https://semver.org): `MAJOR.MINOR.PATCH`.
|
|
270
|
+
|
|
271
|
+
- **PATCH** (`0.1.0 → 0.1.1`): backward-compatible bug fixes.
|
|
272
|
+
- **MINOR** (`0.1.0 → 0.2.0`): backward-compatible new features.
|
|
273
|
+
- **MAJOR** (`0.x → 1.0`, `1.x → 2.0`): breaking changes.
|
|
274
|
+
- `0.y.z` means "unstable" — minor bumps may break. Cut `1.0.0` when the API is committed.
|
|
275
|
+
|
|
276
|
+
Maintain a [Keep a Changelog](https://keepachangelog.com) `CHANGELOG.md` — human-curated, newest first, grouped by Added/Changed/Deprecated/Removed/Fixed/Security.
|
|
277
|
+
|
|
278
|
+
```markdown
|
|
279
|
+
# Changelog
|
|
280
|
+
|
|
281
|
+
## [Unreleased]
|
|
282
|
+
|
|
283
|
+
## [0.2.0] - 2026-06-09
|
|
284
|
+
### Added
|
|
285
|
+
- `Rubino::Client#stream` for incremental responses.
|
|
286
|
+
### Deprecated
|
|
287
|
+
- `Client#fetch_all`; use `#stream`. Removed in 1.0.
|
|
288
|
+
|
|
289
|
+
## [0.1.0] - 2026-05-01
|
|
290
|
+
### Added
|
|
291
|
+
- Initial release.
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
DON'T auto-generate the changelog from raw commit subjects — curate it for humans who need to decide whether to upgrade.
|
|
295
|
+
|
|
296
|
+
## README
|
|
297
|
+
|
|
298
|
+
A gem README should let a reader install and succeed in 60 seconds, in this order:
|
|
299
|
+
|
|
300
|
+
1. One-sentence what-and-why.
|
|
301
|
+
2. Installation: `bundle add rubino` (modern) or the `gem "rubino"` Gemfile line + `gem install rubino`.
|
|
302
|
+
3. Usage — a copy-pasteable minimal example that actually runs.
|
|
303
|
+
4. Configuration options.
|
|
304
|
+
5. Compatibility (supported Ruby/Rails versions).
|
|
305
|
+
6. Development & contributing, License.
|
|
306
|
+
|
|
307
|
+
Show real code, not API tables. Keep the top example small.
|
|
308
|
+
|
|
309
|
+
## Testing a gem
|
|
310
|
+
|
|
311
|
+
Use RSpec (or Minitest — see `references/testing.md`). Test the *public* API against the namespace, not file internals (Zeitwerk autoloads).
|
|
312
|
+
|
|
313
|
+
```ruby
|
|
314
|
+
# spec/rubino_spec.rb
|
|
315
|
+
RSpec.describe Rubino do
|
|
316
|
+
it "has a version" do
|
|
317
|
+
expect(Rubino::VERSION).to match(/\A\d+\.\d+\.\d+/)
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
it "eager loads without naming errors" do
|
|
321
|
+
expect { Rubino::Loader.eager_load }.not_to raise_error
|
|
322
|
+
end
|
|
323
|
+
end
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
### Multi-version testing with Appraisal
|
|
327
|
+
|
|
328
|
+
When your gem must support several versions of a dependency (e.g. Rails 7.1 and 8.0), use the **appraisal** gem to run the suite against each.
|
|
329
|
+
|
|
330
|
+
```ruby
|
|
331
|
+
# Appraisals
|
|
332
|
+
appraise "rails-7.1" do
|
|
333
|
+
gem "rails", "~> 7.1.0"
|
|
334
|
+
end
|
|
335
|
+
appraise "rails-8.0" do
|
|
336
|
+
gem "rails", "~> 8.0.0"
|
|
337
|
+
end
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
```bash
|
|
341
|
+
bundle exec appraisal install # generates gemfiles/*.gemfile + locks
|
|
342
|
+
bundle exec appraisal rspec # runs the suite under every gemfile
|
|
343
|
+
bundle exec appraisal rails-8.0 rspec # just one
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
### CI matrix across Ruby versions
|
|
347
|
+
|
|
348
|
+
```yaml
|
|
349
|
+
# .github/workflows/main.yml
|
|
350
|
+
name: CI
|
|
351
|
+
on: [push, pull_request]
|
|
352
|
+
jobs:
|
|
353
|
+
test:
|
|
354
|
+
runs-on: ubuntu-latest
|
|
355
|
+
strategy:
|
|
356
|
+
fail-fast: false
|
|
357
|
+
matrix:
|
|
358
|
+
ruby: ["3.2", "3.3", "3.4"]
|
|
359
|
+
gemfile: ["gemfiles/rails_7.1.gemfile", "gemfiles/rails_8.0.gemfile"]
|
|
360
|
+
env:
|
|
361
|
+
BUNDLE_GEMFILE: ${{ matrix.gemfile }}
|
|
362
|
+
steps:
|
|
363
|
+
- uses: actions/checkout@v4
|
|
364
|
+
- uses: ruby/setup-ruby@v1
|
|
365
|
+
with:
|
|
366
|
+
ruby-version: ${{ matrix.ruby }}
|
|
367
|
+
bundler-cache: true # bundle install + cache gems
|
|
368
|
+
- run: bundle exec rspec
|
|
369
|
+
- run: bundle exec rubocop
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
Match the Ruby floor in the matrix to `required_ruby_version`.
|
|
373
|
+
|
|
374
|
+
## Building & releasing
|
|
375
|
+
|
|
376
|
+
Bundler's `gem` tasks (loaded by `Bundler::GemHelper.install_tasks` in the Rakefile) drive the release:
|
|
377
|
+
|
|
378
|
+
```bash
|
|
379
|
+
rake build # builds pkg/rubino-0.2.0.gem from the gemspec
|
|
380
|
+
rake install # builds + installs locally for smoke-testing
|
|
381
|
+
rake release # tags vX.Y.Z, pushes the tag, and gem push to rubygems.org
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
`rake release` derives the version from `Rubino::VERSION`, so the release flow is: bump `version.rb` → update `CHANGELOG.md` → commit → `rake release`. It refuses to release with uncommitted changes.
|
|
385
|
+
|
|
386
|
+
### Credentials, API key & MFA
|
|
387
|
+
|
|
388
|
+
- `gem push` reads `~/.gem/credentials` (`chmod 0600`). Get a key with `gem signin` or from rubygems.org → Settings → API keys, scoped to *push only*.
|
|
389
|
+
- Enable account-level **MFA** and set `rubygems_mfa_required = "true"` in metadata (above) so pushes require MFA even if a key leaks.
|
|
390
|
+
|
|
391
|
+
### Trusted publishing / OIDC from CI (preferred)
|
|
392
|
+
|
|
393
|
+
Don't store a long-lived API key in CI secrets. Configure **trusted publishing** on rubygems.org (per-gem, bound to your GitHub repo + workflow), then publish keylessly via OIDC:
|
|
394
|
+
|
|
395
|
+
```yaml
|
|
396
|
+
release:
|
|
397
|
+
runs-on: ubuntu-latest
|
|
398
|
+
if: startsWith(github.ref, 'refs/tags/v')
|
|
399
|
+
permissions:
|
|
400
|
+
id-token: write # required for OIDC
|
|
401
|
+
contents: write
|
|
402
|
+
steps:
|
|
403
|
+
- uses: actions/checkout@v4
|
|
404
|
+
- uses: ruby/setup-ruby@v1
|
|
405
|
+
with: { ruby-version: "3.4", bundler-cache: true }
|
|
406
|
+
- uses: rubygems/release-gem@v1 # exchanges OIDC token, runs gem push
|
|
407
|
+
```
|
|
408
|
+
|
|
409
|
+
This issues a short-lived credential per run — no secret to leak or rotate.
|
|
410
|
+
|
|
411
|
+
### Yanking
|
|
412
|
+
|
|
413
|
+
A pushed version is immutable — you cannot overwrite it. To pull a broken/insecure release:
|
|
414
|
+
|
|
415
|
+
```bash
|
|
416
|
+
gem yank rubino -v 0.2.0 # removes it as an install candidate
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
Yanking does NOT free the version number — you cannot re-push `0.2.0`; ship `0.2.1`. Yank only for serious breakage (security, unusable). Prefer a fast follow-up release for ordinary bugs so existing pins keep resolving.
|
|
420
|
+
|
|
421
|
+
## Backward-compat & deprecation policy
|
|
422
|
+
|
|
423
|
+
Within a major version, don't break the public API. To remove/rename something, deprecate first, remove in the next major.
|
|
424
|
+
|
|
425
|
+
```ruby
|
|
426
|
+
# RIGHT: warn, delegate, document the removal version
|
|
427
|
+
def fetch_all(*args, **kwargs)
|
|
428
|
+
warn "[DEPRECATION] Rubino::Client#fetch_all is deprecated and will be " \
|
|
429
|
+
"removed in 1.0. Use #stream instead.", uplevel: 1
|
|
430
|
+
stream(*args, **kwargs).to_a
|
|
431
|
+
end
|
|
432
|
+
```
|
|
433
|
+
|
|
434
|
+
```ruby
|
|
435
|
+
# WRONG: silently change behavior or delete the method in a minor release
|
|
436
|
+
```
|
|
437
|
+
|
|
438
|
+
- Use `Kernel#warn ... uplevel: 1` so the warning points at the *caller's* line.
|
|
439
|
+
- Record every deprecation under `### Deprecated` in the CHANGELOG with the planned removal version.
|
|
440
|
+
- Treat anything documented in the README/public methods as API. Mark genuinely-internal classes (`@api private` in docs, or a `Rubino::Internal` namespace) so users know what's safe to break.
|
|
441
|
+
- For richer deprecation (Rails-style), `ActiveSupport::Deprecation` provides per-gem deprecator objects. See `references/errors-and-types.md` for warnings/deprecation mechanics.
|
|
442
|
+
|
|
443
|
+
## Quick checklist
|
|
444
|
+
|
|
445
|
+
- Scaffold with `bundle gem <name>` (`--test`, `--ci`, `--linter`); don't hand-roll.
|
|
446
|
+
- `version.rb` contains only `VERSION`; the gemspec `require_relative`s it.
|
|
447
|
+
- `spec.files` from `git ls-files`; never glob the whole directory.
|
|
448
|
+
- Set `required_ruby_version`, metadata URIs, and `rubygems_mfa_required = "true"`.
|
|
449
|
+
- Use `~>` pessimistic constraints in the gemspec; never exact-pin a library dep.
|
|
450
|
+
- Runtime deps via `add_dependency`; dev/test deps via `add_development_dependency`.
|
|
451
|
+
- Let Zeitwerk autoload; match file names to constants; register acronym inflections.
|
|
452
|
+
- User executables in `exe/` + `spec.bindir`; dev helpers in `bin/`; Thor as a thin CLI adapter.
|
|
453
|
+
- One top-level module; no top-level constants/methods; no core monkey-patching.
|
|
454
|
+
- Locate bundled data with `File.expand_path(..., __dir__)`, never CWD-relative.
|
|
455
|
+
- SemVer strictly; curate a Keep a Changelog `CHANGELOG.md`.
|
|
456
|
+
- CI matrix over supported Rubies; Appraisal for multi-version deps; eager-load in CI.
|
|
457
|
+
- Release via `rake release`; prefer OIDC trusted publishing over stored API keys.
|
|
458
|
+
- Versions are immutable — `gem yank` for emergencies; bump for fixes.
|
|
459
|
+
- Deprecate (warn + `uplevel: 1`) before removing; remove only on a major bump.
|