<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="http://www.andywaite.com/feed.xml" rel="self" type="application/atom+xml" /><link href="http://www.andywaite.com/" rel="alternate" type="text/html" /><updated>2026-05-17T12:49:50+00:00</updated><id>http://www.andywaite.com/feed.xml</id><title type="html">Andy Waite</title><subtitle>Ruby and Rails Developer, Toronto
</subtitle><entry><title type="html">Using Claude Code custom slash commands with Ruby</title><link href="http://www.andywaite.com/2025/07/07/using-claude-code-custom-slash-commands-with-ruby.html" rel="alternate" type="text/html" title="Using Claude Code custom slash commands with Ruby" /><published>2025-07-07T00:00:00+00:00</published><updated>2025-07-07T00:00:00+00:00</updated><id>http://www.andywaite.com/2025/07/07/using-claude-code-custom-slash-commands-with-ruby</id><content type="html" xml:base="http://www.andywaite.com/2025/07/07/using-claude-code-custom-slash-commands-with-ruby.html"><![CDATA[<p>For the past few weeks I’ve been using Claude Code extensively in my Ruby development workflow and find it incredibly powerful, especially since the launch of the Sonnet 4 model.</p>

<p>Anthropic recently added support for <a href="https://docs.anthropic.com/en/docs/claude-code/slash-commands#custom-slash-commands">custom slash commands</a>, allowing you to define commonly used prompts as reusable macros.</p>

<p>Based on my experience as a Ruby developer, I’ve created <a href="https://github.com/andyw8/claude_code_slash_commands/tree/main/commands">several custom commands</a> for typical Ruby project workflows, such as:</p>

<ul>
  <li>Configuring CI with GitHub Actions (including Ruby matrix builds)</li>
  <li>Adding RuboCop or Standard</li>
  <li>Bumping the Ruby version across gemspecs, Gemfiles, and CI configs</li>
  <li>Adding common Ruby gems with appropriate configuration</li>
</ul>

<p>Although Claude does a reasonable job for these with a zero-shot prompt, the custom commands allow for better adherence to modern best practices, which are often preferable over what the model was trained on.</p>

<p>Each command has been iteratively refined based on real-world usage across different Ruby projects. (I’ve found that using bullet-point checklists makes Claude more systematic about following instructions).</p>

<p>As these patterns may be valuable to the broader Ruby community, I’ve published a <a href="https://github.com/andyw8/claude_code_slash_commands">claude_code_slash_commands</a> gem (mostly written by Claude Code of course).</p>

<p>The gem provides a simple <code class="language-plaintext highlighter-rouge">install</code> command that copies the slash commands to your local <code class="language-plaintext highlighter-rouge">~/.claude/commands</code> directory. The commands are designed with Ruby development expertise in mind, incorporating common patterns and approaches.</p>

<p>If you have ideas for additional Ruby-focused slash commands, I’d love to see them! Feel free to open a PR on the <a href="https://github.com/andyw8/claude_code_slash_commands">repository</a> to contribute your own commands that could benefit the Ruby community.</p>]]></content><author><name></name></author><summary type="html"><![CDATA[For the past few weeks I’ve been using Claude Code extensively in my Ruby development workflow and find it incredibly powerful, especially since the launch of the Sonnet 4 model.]]></summary></entry><entry><title type="html">Setting up Zed with Ruby LSP</title><link href="http://www.andywaite.com/2025/03/31/setting-up-zed-with-ruby-lsp.html" rel="alternate" type="text/html" title="Setting up Zed with Ruby LSP" /><published>2025-03-31T00:00:00+00:00</published><updated>2025-03-31T00:00:00+00:00</updated><id>http://www.andywaite.com/2025/03/31/setting-up-zed-with-ruby-lsp</id><content type="html" xml:base="http://www.andywaite.com/2025/03/31/setting-up-zed-with-ruby-lsp.html"><![CDATA[<p>(Last updated: 2025-09-04)</p>

<p><a href="https://zed.dev">Zed</a> is a modern, high-performance code editor that’s gaining popularity. After several years of using VS Code, I’ve been trying it out for writing Ruby and I’ve been impressed. With recent rapid development in AI, Zed’s <a href="https://zed.dev/blog/zed-ai">close collaboration with Anthropic</a> makes it a strong contender to VS Code derived editors such as <a href="https://www.cursor.com">Cursor</a>.</p>

<p>In this post you’ll learn how to set up Ruby LSP with Zed, and how to troubleshoot if things aren’t working. I’ll also share some alternatives for features that aren’t currently available in Zed.</p>

<h1 id="setting-up-ruby-support">Setting up Ruby Support</h1>

<p>Zed’s Ruby support comes from the <a href="https://github.com/zed-extensions/ruby">official Ruby extension</a>, created by <a href="https://bsky.app/profile/vitallium.bsky.social">Vitaly Slobodin</a>. You can enable it through <code class="language-plaintext highlighter-rouge">zed: extensions</code>.</p>

<p>As with all Zed extensions, it is written in Rust. Its documentation can be found <a href="https://zed.dev/docs/languages/ruby">here</a>, as part of the official docs.</p>

<h1 id="configuring-ruby-lsp">Configuring Ruby LSP</h1>

<p>Zed has good built-in support for the <a href="https://microsoft.github.io/language-server-protocol/">Language Server Protocol</a>, meaning there’s a lot of tooling already available that can be used.</p>

<p>Zed defaults to using <a href="https://solargraph.org">Solargraph</a> as its language server for Ruby, but my preference is to use <a href="https://github.com/Shopify/ruby-lsp">Ruby LSP</a>, since I’m a <a href="https://github.com/Shopify/ruby-lsp/graphs/contributors">contributor</a>.</p>

<p>You’ll also be able to take advantage of Ruby LSP add-ons such as <a href="https://github.com/Shopify/ruby-lsp-rails">ruby-lsp-rails</a> and <a href="https://github.com/st0012/ruby-lsp-rspec">ruby-lsp-rspec</a>.</p>

<p>It can configured in Zed’s settings as follows:</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"languages"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"Ruby"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
      </span><span class="nl">"language_servers"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">"ruby-lsp"</span><span class="p">]</span><span class="w">
    </span><span class="p">}</span><span class="w">
  </span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>(The docs suggest disabling each language server that you <em>don’t</em> want with <code class="language-plaintext highlighter-rouge">!</code>, and using <code class="language-plaintext highlighter-rouge">"..."</code> to enable the rest, but I find it simpler to just list the ones you <em>do</em> want).</p>

<p>Note: I’ve noticed that some configuration changes don’t seem to apply until you fully quit and reload the editor (<code class="language-plaintext highlighter-rouge">workspace: reload</code> isn’t enough), so you may need to do that in some situations.</p>

<p>To verify things are working, open a Ruby file, then choose <code class="language-plaintext highlighter-rouge">debug: open language server logs</code> from the command menu. The output should look something like this:</p>

<p><img src="/assets/images/zed-ruby-lsp-startup.png" alt="Ruby LSP startup" /></p>

<p>If there are multiple language servers running, then you’ll need to first select the <code class="language-plaintext highlighter-rouge">ruby-lsp</code> log from the menu at the top left.</p>

<p>If ever it seems that Ruby LSP is not running, this is the first place to check for errors.</p>

<h1 id="enabling-diagnostics-linting">Enabling Diagnostics (linting)</h1>

<p>Update: This section previously described a workaround for the lack of pull diagnostics support in Zed, but this has now been implemented and diagnostics are working.</p>

<h1 id="disable-ontypeformatting">Disable onTypeFormatting</h1>

<p>~There is <a href="https://github.com/Shopify/ruby-lsp/issues/2971">known issue</a> in Ruby LSP with cursor placement when trying to type a <code class="language-plaintext highlighter-rouge">|</code> for block arguments, so you may want to disable <code class="language-plaintext highlighter-rouge">onTypeFormatting</code> for now~</p>

<p>Update: This was <a href="https://github.com/zed-extensions/ruby/pull/142">changed</a> to be disabled by default in v0.13 of the extension.</p>

<h1 id="global-vs-local-settings">Global vs Local Settings</h1>

<p>So far I’ve shown settings as being added to the Zed’s global configuration.</p>

<p>If you only ever work on a single Ruby project, then it doesn’t really matter if you all your settings are global. But many of us work on codebases with different configurations, such as for linting and formatting, so having more granular control is often necessary.</p>

<p>You can add project-specific settings to <code class="language-plaintext highlighter-rouge">.zed/settings.json</code> in your project directory. These settings will be merged with the global settings, so normally you’ll only need a few lines of configuration.
For example, you might have some projects that use RuboCop but others that use Standard.</p>

<h1 id="running-tests">Running tests</h1>

<p>One very useful feature when using Ruby LSP in VS Code is <a href="https://shopify.github.io/ruby-lsp/#code-lens">Code Lens</a>, which adds a <code class="language-plaintext highlighter-rouge">Run in Terminal</code> link to each test, and shows the result in the integrated terminal.</p>

<p>This LSP feature is not supported by Zed, but we can use the powerful <a href="https://zed.dev/docs/tasks">Tasks</a> feature to do something similar.</p>

<p>For example, to add a task for running Rails tests, we can create a <code class="language-plaintext highlighter-rouge">.zed/tasks.json</code> file:</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">[</span><span class="w">
  </span><span class="p">{</span><span class="w">
    </span><span class="nl">"label"</span><span class="p">:</span><span class="w"> </span><span class="s2">"test $ZED_RELATIVE_FILE:$ZED_ROW"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"command"</span><span class="p">:</span><span class="w"> </span><span class="s2">"bundle exec rails"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"args"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">"test"</span><span class="p">,</span><span class="w"> </span><span class="s2">"</span><span class="se">\"</span><span class="s2">$ZED_RELATIVE_FILE:$ZED_ROW</span><span class="se">\"</span><span class="s2">"</span><span class="p">],</span><span class="w">
    </span><span class="nl">"tags"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">"ruby-test"</span><span class="p">]</span><span class="w">
  </span><span class="p">}</span><span class="w">
</span><span class="p">]</span><span class="w">
</span></code></pre></div></div>

<p>The test can be run by clicking on the <code class="language-plaintext highlighter-rouge">▷</code> in the gutter, or with the Command-Shift-R keybinding.</p>

<h1 id="advanced-lsp-troubleshooting">Advanced LSP Troubleshooting</h1>

<p>It can sometimes be useful see the underlying requests and responses for the language server. In the <code class="language-plaintext highlighter-rouge">debug: language server logs</code> window, click the second menu, then enable <code class="language-plaintext highlighter-rouge">RPC Messages</code>:</p>

<p><img src="/assets/images/zed-lsp-rpc-logging.png" alt="Ruby LSP startup" /></p>

<h1 id="whats-missing">What’s Missing</h1>

<ul>
  <li>
    <p>If you’re coming from VS Code with Ruby LSP, you might notice some features are not available. There’s two main reasons behind this:</p>

    <ul>
      <li>
        <p>Some Ruby LSP features depends on custom behaviour in the VS Code extension. Those would need to be reimplemented for Zed within the Ruby extension.</p>
      </li>
      <li>
        <p>Some parts of the LSP specification are not yet implemented in Zed.
You can follow <a href="https://github.com/zed-industries/zed/issues/26916">this</a> Zed issue to learn more.</p>
      </li>
    </ul>
  </li>
  <li>
    <s>Changes aren't indexed until Zed is restarted.</s>
    <p>(<a href="https://github.com/Shopify/ruby-lsp/issues/3384">#3384</a>). This is now working.</p>
  </li>
  <li>
    <p>A debugger is in <a href="https://zed.dev/debugger">beta</a>, and there’s a <a href="https://github.com/zed-industries/zed/pull/30126">PR</a> open for supporting rdbg.</p>
  </li>
  <li>Zed supports <a href="https://zed.dev/docs/snippets">Snippets</a> but the Ruby extension doesn’t yet have any. I have a <a href="https://github.com/zed-extensions/ruby/pull/53">PR</a> in progress to add them.</li>
</ul>

<h1 id="other-useful-resources">Other Useful Resources</h1>

<ul>
  <li>For ERB linting, support for Herb has been <a href="https://github.com/zed-extensions/ruby/pull/110">merged</a> and should ship soon.</li>
  <li>For ERB formatting, <a href="https://zed.dev/docs/languages/ruby#erb-formatter">erb-formatter</a> works well.</li>
  <li>To toggle between a test and its implementation, try <a href="https://github.com/MoskitoHero/zed-test-toggle">zed-test-toggle</a>.</li>
  <li>For working with Hotwire, there is the <a href="https://github.com/vitallium/zed-stimulus">Zed Stimulus</a> extension.</li>
  <li>Support for Sorbet was previously available in a <a href="https://github.com/notchairmk/zed-sorbet">separate extension</a> but is now built-in as of v0.9.0 of the Ruby extension.</li>
</ul>]]></content><author><name></name></author><summary type="html"><![CDATA[(Last updated: 2025-09-04)]]></summary></entry><entry><title type="html">Extracting transcripts from Just Press Record</title><link href="http://www.andywaite.com/2024/12/08/extracting-transcripts-from-just-press-record.html" rel="alternate" type="text/html" title="Extracting transcripts from Just Press Record" /><published>2024-12-08T00:00:00+00:00</published><updated>2024-12-08T00:00:00+00:00</updated><id>http://www.andywaite.com/2024/12/08/extracting-transcripts-from-just-press-record</id><content type="html" xml:base="http://www.andywaite.com/2024/12/08/extracting-transcripts-from-just-press-record.html"><![CDATA[<p><a href="https://www.openplanetsoftware.com/just-press-record/">Just Press Record</a> is a nice little app for recording voice memos, available on macOS, iOS and watchOS.</p>

<p>It has the ability to automatically embed transcripts into the recordings. I wanted to extract these out to use with an automated workflow for OmniFocus, a task manager.</p>

<p>With a little bit of investigation, I could see the transcript was represented as a Base64 string contained in JSON. Here’s how to extract it:</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>strings recording.m4a | <span class="nb">tail</span> <span class="nt">-n</span> 1 | <span class="nb">sed</span> <span class="s1">'s/^[^\{]*//'</span> | jq <span class="nt">-r</span> <span class="s1">'._root.txscriptv2.tx._data'</span> | <span class="nb">base64</span> <span class="nt">--decode</span>
</code></pre></div></div>

<p>A brief explanation of how each command is used:</p>
<ul>
  <li><code class="language-plaintext highlighter-rouge">strings</code> to extract any text found within the file</li>
  <li><code class="language-plaintext highlighter-rouge">tail</code> to keep only the last line (the one with the JSON)</li>
  <li><code class="language-plaintext highlighter-rouge">sed</code> to discard the part of the line before the JSON structure</li>
  <li><a href="https://github.com/jqlang/jq"><code class="language-plaintext highlighter-rouge">jq</code></a> to extract the specific part of the JSON containing the encoded text (the <code class="language-plaintext highlighter-rouge">-r</code> flag removes the quotes).</li>
  <li><code class="language-plaintext highlighter-rouge">base64</code> to decode it</li>
</ul>]]></content><author><name></name></author><summary type="html"><![CDATA[Just Press Record is a nice little app for recording voice memos, available on macOS, iOS and watchOS.]]></summary></entry><entry><title type="html">An Introduction to the Ruby LSP Add-on System</title><link href="http://www.andywaite.com/2024/10/03/the-ruby-lsp-addon-system.html" rel="alternate" type="text/html" title="An Introduction to the Ruby LSP Add-on System" /><published>2024-10-03T00:00:00+00:00</published><updated>2024-10-03T00:00:00+00:00</updated><id>http://www.andywaite.com/2024/10/03/the-ruby-lsp-addon-system</id><content type="html" xml:base="http://www.andywaite.com/2024/10/03/the-ruby-lsp-addon-system.html"><![CDATA[<p>I wrote an <a href="https://railsatscale.com/2024-10-03-the-ruby-lsp-addon-system/">article</a>
 for <a href="https://twitter.com/ShopifyEng">Shopify Engineering</a>’s <a href="https://railsatscale.com">Rails at Scale</a> blog:</p>

<blockquote>
  <p>In this post we introduce the Ruby LSP add-on system. We’ll explain the problem it solves, discuss its architecture, showcase some example add-ons, and share our vision for a future addon ecosystem that enhances the Ruby development experience.</p>
</blockquote>

<p><a href="https://railsatscale.com/2024-10-03-the-ruby-lsp-addon-system/">https://railsatscale.com/2024-10-03-the-ruby-lsp-addon-system/</a></p>]]></content><author><name></name></author><summary type="html"><![CDATA[I wrote an article for Shopify Engineering’s Rails at Scale blog:]]></summary></entry><entry><title type="html">Adding Sorbet and Tapioca to a Jumpstart Pro app</title><link href="http://www.andywaite.com/2023/09/17/jumpstart-rails-pro-sorbet-tapioca.html" rel="alternate" type="text/html" title="Adding Sorbet and Tapioca to a Jumpstart Pro app" /><published>2023-09-17T00:00:00+00:00</published><updated>2023-09-17T00:00:00+00:00</updated><id>http://www.andywaite.com/2023/09/17/jumpstart-rails-pro-sorbet-tapioca</id><content type="html" xml:base="http://www.andywaite.com/2023/09/17/jumpstart-rails-pro-sorbet-tapioca.html"><![CDATA[<p>This post describes the process of setting up Sorbet and Tapioca for the <a href="https://jumpstartrails.com">Jumpstart Pro</a> Rails template.</p>

<p>Since Jumpstart Pro is a commercial product, I can’t share the full code, but if you’re a Jumpstart customer you can view the changes in <a href="https://github.com/andyw8/jumpstart-pro-rails/tree/andyw8/sorbet">this branch</a> of my fork. I recommend viewing it as individual commits to understand it step-by-step. The order may not exactly match the blog post, but the end result should be the same.</p>

<p>For this post, I’ll assume you are starting from a freshly generated Jumpstart app. If you have already built your app on top of Jumpstart then it may take some more effort but the overall approach is the same.</p>

<p>I’ll demonstrate the process in incremental steps, so your app can continue to be deployed while type information is still being added. This follows Sorbet’s philosophy of <a href="https://sorbet.org/docs/gradual">Gradual Typing</a>.</p>

<h2 id="preparation">Preparation</h2>

<p>Start by creating a <code class="language-plaintext highlighter-rouge">sorbet</code> branch for the Sorbet migration. This will be fairly short-lived, and only needed for the initial setup.</p>

<h2 id="setting-up-ci">Setting up CI</h2>

<p>Although Jumpstart Pro provides a GitHub Actions CI script, we’ll instead use <a href="https://www.andywaite.com/2022/04/15/reusable-github-actions-rails-workflow.html">setup-rails</a> since it will detect if Sorbet is in use and run additional checks. We’ll also enable the <code class="language-plaintext highlighter-rouge">standard</code> option, since that’s what Jumpstart uses instead of RuboCop.</p>

<div class="language-yml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">name</span><span class="pi">:</span> <span class="s">Verify</span>
<span class="na">on</span><span class="pi">:</span> <span class="pi">[</span><span class="nv">push</span><span class="pi">,</span> <span class="nv">pull_request</span><span class="pi">]</span>

<span class="na">jobs</span><span class="pi">:</span>
  <span class="na">verify</span><span class="pi">:</span>
    <span class="na">uses</span><span class="pi">:</span> <span class="s">setup-rails/setup-rails/.github/workflows/verify.yml@main</span>
    <span class="na">with</span><span class="pi">:</span>
      <span class="na">run-before-tests</span><span class="pi">:</span> <span class="s">sudo apt-get install -y -qq libvips</span>
      <span class="na">standard</span><span class="pi">:</span> <span class="no">true</span>
</code></pre></div></div>

<p>(I also had to disable parallel testing by commenting-out the <code class="language-plaintext highlighter-rouge">parallelize</code> line in <code class="language-plaintext highlighter-rouge">test_helper.rb</code>, as I found it was causing the tests to hang. I haven’t had a chance yet to look into the cause.)</p>

<h2 id="basic-setup">Basic Setup</h2>

<p>Overall, the setup for Jumpstart Pro is not so different than for any other Rails app, but the optional dependencies complicate things a little: If there is code that references a gem that isn’t installed, then typechecking will fail, even if it’s within a <code class="language-plaintext highlighter-rouge">defined?</code> check. To simplify things for this guide, we will open the Jumpstart configuration page and enable the following features:</p>

<ul>
  <li>Payment Processor: Stripe</li>
  <li>Background Queue: Sidekiq</li>
  <li>ActsAsTenant</li>
  <li>Facebook Omniauth Provider</li>
</ul>

<p>This will result in some additions to <code class="language-plaintext highlighter-rouge">Gemfile.lock</code> which you should commit.</p>

<p>Next, we’ll add the <code class="language-plaintext highlighter-rouge">sorbet-static-and-runtime</code> and <code class="language-plaintext highlighter-rouge">tapioca</code> gems to the <code class="language-plaintext highlighter-rouge">Gemfile</code>, run <code class="language-plaintext highlighter-rouge">bundle</code> then <code class="language-plaintext highlighter-rouge">bundle exec tapioca init</code>.</p>

<p>The <code class="language-plaintext highlighter-rouge">init</code> command can take a long time to run (10 minutes or more), and it may seem like it has frozen. Have patience!</p>

<p>You may be alarmed by the huge number of RBI files this creates, but you will very rarely need to interact with them.</p>

<p>After this is complete, we can commit everything. Let’s now run the typechecker:</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>bundle <span class="nb">exec </span>srb tc
</code></pre></div></div>

<p>We should see about 20 errors. It’s common to encounter errors when setting up Sorbet initially, usually due to known limitations in Sorbet or Tapioca.</p>

<p>In the case of Jumpstart, several are due <a href="https://sorbet.org/docs/error-reference#5068">potentially ambiguous</a> definitions, which are easily fixed by using the full version of the definition. For example instead of:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">module</span> <span class="nn">Admin</span>
  <span class="k">class</span> <span class="nc">User::ImpersonatesController</span> <span class="o">&lt;</span> <span class="no">Admin</span><span class="o">::</span><span class="no">ApplicationController</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>
<p>we need to write:</p>
<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">module</span> <span class="nn">Admin</span>
  <span class="k">module</span> <span class="nn">User</span>
    <span class="k">class</span> <span class="nc">ImpersonatesController</span> <span class="o">&lt;</span> <span class="no">Admin</span><span class="o">::</span><span class="no">ApplicationController</span>
    <span class="k">end</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>Other errors are because some parts of the gem are not required by default. We need to add an entry to Tapioca’s <code class="language-plaintext highlighter-rouge">require.rb</code>:</p>

<div class="language-yml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># sorbet/tapioca/require.rb</span>
<span class="s">require "administrate/base_dashboard"</span>
</code></pre></div></div>

<p>and then re-run <code class="language-plaintext highlighter-rouge">bundle exec tapioca gem administrate</code>.</p>

<p>After this there should be only handful of remaining errors, which are due to the Sorbet limitation that “include must only contain constant literals”. We can work around this by marking those calls as unsafe:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="no">T</span><span class="p">.</span><span class="nf">unsafe</span><span class="p">(</span><span class="nb">self</span><span class="p">).</span><span class="nf">include</span> <span class="no">Engine</span><span class="p">.</span><span class="nf">routes</span><span class="p">.</span><span class="nf">url_helpers</span>
</code></pre></div></div>

<p>At this point you can push to CI and everything should be green again. If you wish, you can merge the <code class="language-plaintext highlighter-rouge">sorbet</code> branch into <code class="language-plaintext highlighter-rouge">main</code> and continue the remaining work in other branches.</p>

<h2 id="clearing-the-todo-list">Clearing the TODO list.</h2>

<p>Although we are no longer seeing any typechecking errors, some things are being ignored because they are listed in <code class="language-plaintext highlighter-rouge">todo.rbi</code> which was generated by <code class="language-plaintext highlighter-rouge">sorbet init</code>. We should aim to eliminate these before continuing.</p>

<p>We intentionally never manually edit <code class="language-plaintext highlighter-rouge">todo.rbi</code> - we’ll make a change, and then regenerate it, to gradually reduce the number of entries.</p>

<p>Many of the remaining entries in <code class="language-plaintext highlighter-rouge">todo.rbi</code> are due to optional gems, where they are conditionally referenced in an initializer. We could just delete those, but that would make it a little tricker pulling in changes from upstream in Jumpstart Pro. The approach I suggest is using Ruby’s <code class="language-plaintext highlighter-rouge">__END__</code> keywords. It indicates that the code in the file has ended, and so Ruby (and Sorbet) will ignore it:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp">__END__

Bugsnag.configure do |config|
  config.api_key = Rails.application.credentials.dig(:bugsnag, :api_key)
end
</span></code></pre></div></div>

<p>Next, we have some entries in <code class="language-plaintext highlighter-rouge">todo.rbi</code> that relate to the Devise and Noticed gems. For gems that make use of metaprogramming, we often need to give Sorbet some help by adding <a href="https://sorbet.org/docs/rbi">shims</a>.</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># sorbet/shims.rbi</span>
<span class="k">class</span> <span class="nc">Devise::OmniauthCallbacksController</span><span class="p">;</span> <span class="k">end</span>
<span class="k">class</span> <span class="nc">Devise::RegistrationsController</span><span class="p">;</span> <span class="k">end</span>
<span class="k">class</span> <span class="nc">Devise::SessionsController</span><span class="p">;</span> <span class="k">end</span>

<span class="k">class</span> <span class="nc">Noticed::NotificationChannel</span><span class="p">;</span> <span class="k">end</span>
</code></pre></div></div>

<p>For <code class="language-plaintext highlighter-rouge">Minitest::Mock</code> and <code class="language-plaintext highlighter-rouge">Sidekiq::Web</code>, we again need to add entries to <code class="language-plaintext highlighter-rouge">require.rb</code> then regenerate the RBIs.</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># sorbet/tapioca/require.rb</span>
<span class="nb">require</span> <span class="s2">"minitest/mock"</span>
<span class="nb">require</span> <span class="s2">"sidekiq/web"</span>
</code></pre></div></div>

<p>At this point, there should be no more entries in <code class="language-plaintext highlighter-rouge">todo.rbi</code> and running <code class="language-plaintext highlighter-rouge">tapioca todo</code> will delete it.</p>

<h2 id="standard">Standard</h2>

<p>Although Jumpstart uses Standard rather than RuboCop, there are some useful cops in <code class="language-plaintext highlighter-rouge">rubocop-sorbet</code>, so we will add  that as a dependency.</p>

<p>Then in <code class="language-plaintext highlighter-rouge">.standard.yml</code>, we’ll use Standard’s <a href="https://blog.testdouble.com/posts/2023-01-19-super-standard-adding-gem-extensions-and-custom-rules/">extend_config</a> feature to reference a RuboCop Sorbet configuration file:</p>

<div class="language-yml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># .standard.yml</span>
<span class="na">extend_config</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="s">.rubocop_sorbet.yml</span>
</code></pre></div></div>

<p>The config will look like this:</p>

<div class="language-yml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># .rubocop_sorbet.yml</span>
<span class="na">require</span><span class="pi">:</span> <span class="s">rubocop-sorbet</span>

<span class="na">AllCops</span><span class="pi">:</span>
  <span class="na">NewCops</span><span class="pi">:</span> <span class="s">disable</span>
  <span class="na">Exclude</span><span class="pi">:</span>
    <span class="pi">-</span> <span class="s">lib/jumpstart/test/dummy/**/*</span>
<span class="na">Sorbet/ConstantsFromStrings</span><span class="pi">:</span>
  <span class="na">Exclude</span><span class="pi">:</span>
    <span class="pi">-</span> <span class="s1">'</span><span class="s">app/models/connected_account.rb'</span>
</code></pre></div></div>

<p>We’ll ignore the Dummy app used by the internal jumpstart gem, since that should be treated as a separate application.</p>

<p>We also we need to disable the <code class="language-plaintext highlighter-rouge">ConstantsFromStrings</code> check for one file, due to Sorbet’s limitations for <code class="language-plaintext highlighter-rouge">const_get</code>.</p>

<p>With that done, we can now run <code class="language-plaintext highlighter-rouge">bundle exec standardrb --fix</code> which will add a <code class="language-plaintext highlighter-rouge">typed: false</code> entry to each file. This happens because rubocop-sorbet’s default configuration enables the <code class="language-plaintext highlighter-rouge">Sorbet/FalseSigil</code> cop, which ensures all files are at a strictness of a least <code class="language-plaintext highlighter-rouge">false</code>.</p>

<p>On its own that doesn’t do anything, but it prepares the way so that we can use Spoom.</p>

<h2 id="spoom">Spoom</h2>

<p>Spoom consists of several tools, one of which is the <code class="language-plaintext highlighter-rouge">bump</code> command:</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>bundle <span class="nb">exec </span>spoom bump
</code></pre></div></div>
<p>This helps us discover which files can be ‘bumped’ up a level of typing.</p>

<p>You should see that a large number are now marked as <code class="language-plaintext highlighter-rouge"># typed: true</code>, without us having to do any work.</p>

<h2 id="next-steps">Next Steps</h2>

<p>At this point, you can search for <code class="language-plaintext highlighter-rouge"># typed: false</code> in <code class="language-plaintext highlighter-rouge">*.rb</code> and you’ll see there are around 100 files remaining that aren’t yet typed. Resolving all those is outside the scope of this post, but you now have a strong starting point.</p>

<p>You’ll notice that we haven’t written any signatures yet. But even without those, we can start benefiting from Sorbet’s checks for things such as calls to non-existing methods, or unreachable code.</p>]]></content><author><name></name></author><summary type="html"><![CDATA[This post describes the process of setting up Sorbet and Tapioca for the Jumpstart Pro Rails template.]]></summary></entry><entry><title type="html">A GitHub Actions Rails CI Workflow in 5 lines</title><link href="http://www.andywaite.com/2022/04/15/reusable-github-actions-rails-workflow.html" rel="alternate" type="text/html" title="A GitHub Actions Rails CI Workflow in 5 lines" /><published>2022-04-15T00:00:00+00:00</published><updated>2022-04-15T00:00:00+00:00</updated><id>http://www.andywaite.com/2022/04/15/reusable-github-actions-rails-workflow</id><content type="html" xml:base="http://www.andywaite.com/2022/04/15/reusable-github-actions-rails-workflow.html"><![CDATA[<p>A few years ago, Matt Swanson wrote a <a href="https://boringrails.com/articles/building-a-rails-ci-pipeline-with-github-actions/">great post</a> on setting up Rails CI on GitHub Actions. It quickly became my go-to reference for setting up CI for new apps.</p>

<p>Over time, I made few updates and adjustments to it, so whenever I started a new project I would copy the config from one of my older projects.
But this meant each project gradually became inconsistent, and some got updated more than others.</p>

<p>I wanted to have one single base workflow for all my apps, so that if I made a change, all the apps could easily benefit from it.</p>

<h2 id="reusable-workflows">Reusable Workflows</h2>

<p>In November 2021, GitHub announced that <a href="https://github.blog/2021-11-29-github-actions-reusable-workflows-is-generally-available/">Reusable Workflows</a> was generally available.</p>

<p>Although GitHub Actions has supported composite actions for a long time, Reusable Workflows allows for a much more concise configuration, and the ability to reference a whole workflow from another repository, rather than having to build up each step individually.</p>

<p><a href="https://github.blog/2022-02-10-using-reusable-workflows-github-actions/">Read more about it on GitHub’s blog</a>.</p>

<h2 id="introducing-setup-rails">Introducing setup-rails</h2>

<p>Using the Reusable Workflows feature, I’ve created <strong><a href="https://github.com/andyw8/setup-rails">setup-rails</a></strong> for quickly and easily enabling CI for Rails apps.</p>

<p>By creating a single file in your repo, e.g. <code class="language-plaintext highlighter-rouge">.github/workflows/verify.yml</code>, with the contents below, you should have a working CI workflow which configures the database and runs your app’s tests:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">name</span><span class="pi">:</span> <span class="s">Verify</span>
<span class="na">on</span><span class="pi">:</span> <span class="pi">[</span><span class="nv">push</span><span class="pi">]</span>

<span class="na">jobs</span><span class="pi">:</span>
  <span class="na">verify</span><span class="pi">:</span>
    <span class="na">uses</span><span class="pi">:</span> <span class="s">andyw8/setup-rails/.github/workflows/verify.yml@v1</span>
</code></pre></div></div>

<p>You can take a look at <a href="https://github.com/andyw8/setup-rails-example-app">this example app</a> to see it in action.</p>

<p>The first time you run the workflow may be slow, but subsequent runs should be much faster once the dependencies are cached.</p>

<p>I’ve also shared a <a href="https://railsbytes.com/templates/VMys8A">Railsbyte</a> so that you can set this up with one command:</p>

<p><code class="language-plaintext highlighter-rouge">rails app:template LOCATION="https://railsbytes.com/script/VMys8A"</code></p>

<h2 id="principles-and-configuration">Principles and Configuration</h2>

<p>I designed <code class="language-plaintext highlighter-rouge">setup-rails</code> so it should work at a basic level with no configuration needed for most apps.</p>

<p>There are a few options you can enable depending on your app, such as RuboCop, Bundler Audit and RSpec - see the <a href="https://github.com/andyw8/setup-rails">README</a> for details.</p>

<p>Currently only Postgres is supported but I’d like to expand to cover at least MySQL.</p>

<h2 id="enhancements">Enhancements</h2>

<p>Building upon Matt’s great starting point, I made a few updates:</p>

<ul>
  <li>Uses the latest versions of <code class="language-plaintext highlighter-rouge">setup-node</code> and <code class="language-plaintext highlighter-rouge">setup-ruby</code></li>
  <li>Adds support for <code class="language-plaintext highlighter-rouge">rails test</code> (e.g. Minitest) as well as RSpec</li>
  <li><a href="https://github.com/andyw8/setup-rails/blob/main/.github/dependabot.yml">Enabled Dependabot</a> for updates of the actions that <code class="language-plaintext highlighter-rouge">setup-rails</code> depends on</li>
  <li>Uses <code class="language-plaintext highlighter-rouge">setup-node</code>’s JavaScript caching rather than a custom approach</li>
  <li>Skips <code class="language-plaintext highlighter-rouge">development</code> gems when running Bundler to avoid unnecessary work.</li>
</ul>

<h2 id="get-involved">Get Involved</h2>

<p>I’m already using this on most of my apps, but I’d love to get wider feedback.</p>

<p>Please try <a href="https://github.com/andyw8/setup-rails">setup-rails</a> on your app and let me know how it works for you. Issues and pull requests are welcome!</p>]]></content><author><name></name></author><summary type="html"><![CDATA[A few years ago, Matt Swanson wrote a great post on setting up Rails CI on GitHub Actions. It quickly became my go-to reference for setting up CI for new apps.]]></summary></entry><entry><title type="html">My Ruby Kata Template</title><link href="http://www.andywaite.com/2022/04/09/ruby-kata-template.html" rel="alternate" type="text/html" title="My Ruby Kata Template" /><published>2022-04-09T00:00:00+00:00</published><updated>2022-04-09T00:00:00+00:00</updated><id>http://www.andywaite.com/2022/04/09/ruby-kata-template</id><content type="html" xml:base="http://www.andywaite.com/2022/04/09/ruby-kata-template.html"><![CDATA[<p>Recently I’ve been aiming to do more <a href="http://codekata.com/">katas</a>, both in my personal time, and at <a href="https://shopify.engineering/">work</a> in our <a href="https://github.com/97-things/97-things-every-programmer-should-know/blob/master/en/thing_22/README.md">Deliberate Practice</a> group.</p>

<p>I noticed that whenever I was starting on a new kata, I was doing the same basic setup repeatedly before being able to write a first test. I also felt I was missing some of the configuration that my other ‘real’ projects benefited from.</p>

<p>This led me to create <a href="https://github.com/andyw8/ruby-kata-template">ruby-kata-template</a>. By clicking on <strong>Use this template</strong>, I get a new blank project in seconds, configured to my preferences.</p>

<p>I don’t expect my preferences to match everyone else’s, so I encourage you to fork it and customize to your own ideal template.</p>

<h2 id="features">Features</h2>

<h3 id="starting-point">Starting Point</h3>

<p>There is an empty Project (<code class="language-plaintext highlighter-rouge">project.rb</code>) and a corresponding test (<code class="language-plaintext highlighter-rouge">project_test.rb</code>). I expect the first step in many katas will be to rename these.</p>

<h3 id="directory-structure">Directory Structure</h3>

<p>I’ve followed the standard Ruby convention of the implementation being in <code class="language-plaintext highlighter-rouge">lib/</code> and the tests in <code class="language-plaintext highlighter-rouge">test/</code></p>

<h3 id="minitest">Minitest</h3>

<p>Although Minitest is already provided as a Bundled gem with Ruby, it’s included in the <code class="language-plaintext highlighter-rouge">Gemfile</code> to ensure we’re on the latest version. (I’ve used RSpec extensively in the past, but my current preference is Minitest).</p>

<h3 id="activesupporttestcase">ActiveSupport::TestCase</h3>

<p><code class="language-plaintext highlighter-rouge">ActiveSupport::TestCase</code> is part of Rails but here we are using it independently. It allows you to write ‘declarative’ tests, e.g. <code class="language-plaintext highlighter-rouge">test "it works" do...</code> rather than <code class="language-plaintext highlighter-rouge">def test_it_works</code>, which I feel is more readable.</p>

<h3 id="rake-task">Rake Task</h3>

<p>It’s common on Ruby apps to have <code class="language-plaintext highlighter-rouge">rake</code> or <code class="language-plaintext highlighter-rouge">rake test</code>, so we provide that. It has support for <a href="https://minitest.rubystyle.guide/#file-naming">both MiniTest naming conventions</a> (<code class="language-plaintext highlighter-rouge">test_*.rb</code> and <code class="language-plaintext highlighter-rouge">*_test.rb</code>)</p>

<h3 id="standard">Standard</h3>

<p><a href="https://github.com/testdouble/standard">Standard</a> is an opinionated RuboCop configuration.</p>

<p>For me, the real value of Standard (or RuboCop) is only apparent when you have two things configured in your editor:</p>
<ul>
  <li>Immediate feedback (your editor highlights problems as you code)</li>
  <li>Auto-formatting (you can fix issues with one single shortcut, or automatically when the file is saved).</li>
</ul>

<p>There are various ways to set up this, but the template uses <code class="language-plaintext highlighter-rouge">rubocop-lsp</code>.</p>

<h3 id="rubocop-lsp">rubocop-lsp</h3>

<p><a href="https://rubygems.org/gems/rubocop-lsp">rubocop-lsp</a> is a gem which implements the <a href="https://en.wikipedia.org/wiki/Language_Server_Protocol">Language Server Protocol</a>. It allows for a close integration between your editor with RuboCop. It avoids the overhead of starting RuboCop, meaning linting or auto-correction is near-instant.</p>

<p>To use it, you’ll also need an plugin or extension for your editor. The template is set up to recommend the VS Code <a href="https://marketplace.visualstudio.com/items?itemName=Shopify.rubocop-lsp">Shopify.rubocop-lsp</a> extension.</p>

<h2 id="solargraph">Solargraph</h2>

<p>Solargraph adds IDE-like experience for your editor. It provides helpful features such as a content-aware autocompletion, and documentation for the Ruby language.</p>

<p>However, I often run into problems with it, so I was on the fence about including it.</p>

<p>It’s not as powerful as <a href="https://sorbet.org/">Sorbet</a>, so it’s unable to infer types unless they are a Ruby primitive (String, Array, etc.)</p>

<p><img src="images/solargraph-example.png" alt="Solargraph example" /></p>

<h3 id="and-more">And more</h3>

<p>Take a look at the <a href="https://github.com/andyw8/ruby-kata-template#readme">README</a> to learn what else is included.</p>]]></content><author><name></name></author><summary type="html"><![CDATA[Recently I’ve been aiming to do more katas, both in my personal time, and at work in our Deliberate Practice group.]]></summary></entry><entry><title type="html">Defining your team’s policy for broken builds</title><link href="http://www.andywaite.com/2020/03/16/define-your-teams-broken-build-policy.html" rel="alternate" type="text/html" title="Defining your team’s policy for broken builds" /><published>2020-03-16T00:00:00+00:00</published><updated>2020-03-16T00:00:00+00:00</updated><id>http://www.andywaite.com/2020/03/16/define-your-teams-broken-build-policy</id><content type="html" xml:base="http://www.andywaite.com/2020/03/16/define-your-teams-broken-build-policy.html"><![CDATA[<p>To deliver working software incrementally, and at a sustainable pace, it’s critical to keep the master build passing at all times.</p>

<p>A failing build is demoralizing and disruptive for the team.
Developers and testers will lose confidence in the correctness of the system.
They may be unable to know if a failing test was due to their change, or to something else.</p>

<p>Keeping the master build passing should take priority over almost everything else.
The whole team should feel responsible for it, and when the build fails, the whole team should be alerted.
It shouldn’t be left to a ‘devops’ team, or only to senior developers.
One option is to configure your CI to post notifications to your team’s Slack channel for high visibility.</p>

<p>We can adopt several practices to help ensure master stays passing:</p>

<ul>
  <li>Performing code reviews to detect potentially problematic tests</li>
  <li>Running linters to catch common problem areas, such as rubocop-rspec</li>
  <li>Using features such as <a href="https://developer.github.com/v3/checks/">GitHub Checks</a> to enforce a passing build before the branch can be merged into master.</li>
</ul>

<p>But even with these practices, things can still go wrong:</p>

<ul>
  <li>We can have intermittent failures due to factors such as time of day or concurrency.</li>
  <li>We can have a <a href="https://www.martinfowler.com/bliki/SemanticConflict.html">semantic conflict</a>, where behaviour is correct on individual branches but incorrect when they’re combined.</li>
  <li>We can have infrastructure failures unrelated to the code.</li>
</ul>

<p>It’s important to establish a clear process for what should be done when the master build fails.
The details of this should be discussed and agreed by your team, but I’ll discuss one starting point.</p>

<p>In most cases, the most recent merge will be the one that broke the build.
If the author of the most recent merge hasn’t reacted to the failure, send a gentle nudge.</p>

<p>The first thing the author should do is pull the latest master, and check if the test passes locally.
If it fails, that gives a good starting point. If it passes, we’ll have to broaden the investigation.</p>

<p>While this in progress, there are two key things to keep in mind:</p>

<ul>
  <li>Getting the build back to passing as soon as possible</li>
  <li>Keeping the team informed, and sharing an ETA if feasible</li>
</ul>

<p>If the team isn’t aware of work going on, a developer might assume that a failure was due to their change.
Or another developer may start investigating the failure, unaware that someone else is already on it.
Both could result in wasted time.</p>

<p>The next step is more nuanced. It may need discussion with the team, and could depend on the nature of your project:</p>

<ul>
  <li>Should you revert?</li>
  <li>Should you temporarily disable the test?</li>
  <li>Should you delete a flawed test?</li>
</ul>

<p>Have a clear written policy for this is useful, for example in your wiki or README.</p>

<p>If the fix is going to take some time to resolve, you should aim to give updates at least every hour or so.</p>

<p>Once the problem is resolved, take some time to reflect.
Was there a particular practice which caused this? Could you make some change to improve things?
There’s almost always some learning opportunity.</p>]]></content><author><name></name></author><summary type="html"><![CDATA[To deliver working software incrementally, and at a sustainable pace, it’s critical to keep the master build passing at all times.]]></summary></entry><entry><title type="html">Avoiding common cron pitfalls when scaling Rails</title><link href="http://www.andywaite.com/2020/03/15/cron-scaling-rails.html" rel="alternate" type="text/html" title="Avoiding common cron pitfalls when scaling Rails" /><published>2020-03-15T00:00:00+00:00</published><updated>2020-03-15T00:00:00+00:00</updated><id>http://www.andywaite.com/2020/03/15/cron-scaling-rails</id><content type="html" xml:base="http://www.andywaite.com/2020/03/15/cron-scaling-rails.html"><![CDATA[<p>A common theme in business applications is the need for some kind of periodic task to run at a fixed interval, such as daily or weekly.
This is often used for activities such as billing, pushing data to other systems, or integrating with a third-party API.</p>

<p>The de facto tool to use for this is cron. It’s somewhat archaic, but provides a reliable mechanism to declaratively define job schedules.
The popular <a href="https://github.com/javan/whenever">whenever</a> gem provides an DSL to make this easy to use with Ruby.</p>

<p>Simple tasks such as clearing a cache are a great match for cron.
They run quickly and don’t require significant system resources.</p>

<p>The difficulty comes when cron is used to execute tasks which rely on the Rails application.
It’s easy to make use of cron for this, because we can call the <code class="language-plaintext highlighter-rouge">rails runner</code> command to invoke a method on a class.</p>

<p>On a small app, this approach may be fine, but when scaling up there are some serious drawbacks.</p>

<p>Whenever we use <code class="language-plaintext highlighter-rouge">rails runner</code>, we’re launching a completely separate instance of the application.
Let’s say your Rails app typically uses around 200MB of memory.
To allow for some growth, we provision a server with 512MB of memory.
If you schedule a cron job, the server’s memory usage will temporarily spike to around 400MB.</p>

<p>This might not even be noticed at first.
Even if the machine runs short on memory, it can temporarily make use of swap space on disk.
This might happen in the middle of night, when traffic is already low.</p>

<p>But consider what happens once you have more scheduled jobs:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">CalculateUsage</code> runs daily at 2am</li>
  <li><code class="language-plaintext highlighter-rouge">GenerateReports</code> runs weekly at 2am (every Monday)</li>
  <li><code class="language-plaintext highlighter-rouge">CreateInvoices</code> runs monthly at 2am (first Monday of each month)</li>
</ul>

<p>This means that once a month, all three jobs will be triggered at the same time.
Your system needs to have enough capacity to run three instances, in addition to the main app.</p>

<p>This could mean you need to over-provision your servers by 4x. That extra capacity
will be idle most of the time. On a large site, you could be spending thousands
of extra dollars per month. Or if you don’t sufficiently provision, then you risk crashing your site.</p>

<p>This risk is often exacerbated by the nature of scheduled jobs.
A web request typically lasts only for a few seconds at most.
But a job may run for a much longer period, causing a large spike in memory use.</p>

<h2 id="a-first-approach">A First Approach</h2>

<p>Let’s take a step back, and consider how we schedule jobs. Is it critical that
each jobs runs at 2am? Probably not. The key thing probably that the job is
complete by the beginning of the business day.</p>

<p>A common first reaction to this problem is to try ‘pad out’ the jobs to prevent them overlapping.
For example, instead of running each job at 2am, you run the first at 2.00am, the next at 2.05am, and the next at 2.10am.</p>

<p>While this may provide a short term fix, it’s not a sustainable solution.
As your data grows, the time it takes to run each job will creep up, and will start to overlap again.
You’ll end up playing <a href="https://en.wikipedia.org/wiki/Whac-A-Mole">Whac-A-Mole</a> shifting jobs around.</p>

<h2 id="a-better-approach">A Better Approach</h2>

<p>As with many scaling problems, we can handle growth better if we can scale horizontally and add additional machines.</p>

<p>We can achieve this with a distributed queue.
In Rails, we typically use tools such as Sidekiq, Resque or Delayed Job.</p>

<p>We could even configure this to auto-scale to handle varying workloads.</p>

<p>Instead of using cron to execute the jobs, we’ll use it to only enqueue them, e.g.:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">job</span> <span class="o">=</span> <span class="no">MonthlyReport</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="no">Date</span><span class="p">.</span><span class="nf">today</span><span class="p">)</span>
<span class="no">Delayed</span><span class="o">::</span><span class="no">Job</span><span class="p">.</span><span class="nf">enqueue</span><span class="p">(</span><span class="n">job</span><span class="p">,</span> <span class="ss">queue: </span><span class="s1">'cron'</span><span class="p">)</span>
</code></pre></div></div>
<p>We’ll also benefit from built-in support for retries, although this does require some care to write jobs which are idempotent.</p>

<p>This approach still involves booting up a separate Rails instance, but it’s a fast operation.
Spacing jobs one minute apart should be sufficient to prevent overlaps.</p>

<p>You may later run into another problem of timeouts from jobs being too large.
That can often be handled with a map-reduce approach, but that’s beyond the scope of this post.</p>

<h2 id="further-improvement">Further Improvement</h2>

<p>Instead of having to boot up an instance of the app just to enqueue a job, there’s another approach we can take.</p>

<p>We can add an internal API endpoint, such as <code class="language-plaintext highlighter-rouge">/api/internal/jobs</code>. We can make an HTTP post, specifying the job to be enqueued.
That means cron only needs to execute a call to something like curl, which uses vastly less resources.
Obviously you should add some kind of authentication or restriction on the endpoint to avoid any possibility of a DOS attack.</p>]]></content><author><name></name></author><summary type="html"><![CDATA[A common theme in business applications is the need for some kind of periodic task to run at a fixed interval, such as daily or weekly. This is often used for activities such as billing, pushing data to other systems, or integrating with a third-party API.]]></summary></entry><entry><title type="html">Speed up your TDD by refactoring rails_helper.rb</title><link href="http://www.andywaite.com/2020/03/14/tdd-rails-helper.html" rel="alternate" type="text/html" title="Speed up your TDD by refactoring rails_helper.rb" /><published>2020-03-14T00:00:00+00:00</published><updated>2020-03-14T00:00:00+00:00</updated><id>http://www.andywaite.com/2020/03/14/tdd-rails-helper</id><content type="html" xml:base="http://www.andywaite.com/2020/03/14/tdd-rails-helper.html"><![CDATA[<p>Having a fast feedback cycle is critical element of successful test-driven development.
You should be able to run a single test within one second.</p>

<p>If you use rspec-rails, and your app has been around for more than a few years, the chances are that your <code class="language-plaintext highlighter-rouge">rails_helper.rb</code> has accumulated a lot of cruft.
This increases the time it takes to run a single test, probably to several seconds at least.
As this usually happens gradually, it often goes unnoticed by those who work in a codebase every day.
Developers may become accustomed to waiting ten seconds or more to run a single test.</p>

<p>The default <code class="language-plaintext highlighter-rouge">rails_helper.rb</code> generated by the rspec-rails installer is minimal and fast. There are a variety of things which tend to be added which slow it down:</p>

<ul>
  <li>Seeds tasks (such as from seed_fu).</li>
  <li>Requiring of all files within <code class="language-plaintext highlighter-rouge">spec/support</code> (older versions of rspec-rails defaulted to this but this approach is now discouraged by the RSpec team).</li>
  <li>Global RSpec hooks such as <code class="language-plaintext highlighter-rouge">Before</code> and <code class="language-plaintext highlighter-rouge">After</code></li>
  <li>Libraries to manage database state, such as DatabaseCleaner</li>
  <li>Setup for browser testing tools, such as Capybara</li>
  <li>Miscellaneous other testing support libraries.</li>
</ul>

<p>Some of these will have a much bigger impact than others.
You can use profiling tools to better understand the contribution of each.</p>

<h2 id="overview">Overview</h2>

<p>The key to speeding up the TDD cycle is to only load what’s needed for a specific test.
This strays from Rails’ convention of having everything auto-loaded, but I would argue it’s a worthwhile trade-off.</p>

<p>(Note that this will probably not have any impact on your overall test suite time. The focus here is on the time for running an individual test or test file).</p>

<p>Refactoring your whole test suite at once could take some time, perhaps several days for a large app.
We want to do it gradually, in small steps, so that test suite stays green. Here’s how.</p>

<h2 id="implementation">Implementation</h2>

<p>First, we rename the existing <code class="language-plaintext highlighter-rouge">rails_helper.rb</code> to something like <code class="language-plaintext highlighter-rouge">legacy_rails_helper.rb</code>.</p>

<p>We then update the existing references to that file with the new filename, which should be a simple global search and replace in your editor.</p>

<p>Run your tests suite to ensure everything is still passing.</p>

<p>Next, we create a ‘clean slate’ <code class="language-plaintext highlighter-rouge">rails_helper.rb</code>. An easy to do this is by re-running the rspec-rails generator, i.e. <code class="language-plaintext highlighter-rouge">rails generate rspec:install</code>.</p>

<p>Now, find a relatively basic test in your test suite. It’s usually easier to start with focused unit tests rather than integration tests, since they typically have more dependencies.</p>

<p>Change the test to run with the new <code class="language-plaintext highlighter-rouge">rails_helper.rb</code>. If it passes, then great, we’re done.</p>

<p>Note that it’s important to verify that the tests passes individually. If you run the whole suite, a previously run test may have already loaded a necessary dependency. Since RSpec runs the tests in a random order, this may result in a false positive.</p>

<p>But what if the test fails? Often the test output will indicate that the failure is due to a missing dependency.</p>

<p>There are a few courses of action you can take to resolve this:</p>

<ul>
  <li>You can determine what lines in <code class="language-plaintext highlighter-rouge">legacy_rails_helper.rb</code> are needed, and copy them to your <code class="language-plaintext highlighter-rouge">rails_helper.rb</code>.</li>
  <li>You can use custom hooks so that particular lines are only executed for tests that are tagged with that hook name.</li>
  <li>You can copy only what’s required for that specific test.</li>
</ul>

<p>Let’s talk about the pros and cons of each.</p>

<p>If we always copy the code back into <code class="language-plaintext highlighter-rouge">rails_helper.rb</code> then we’ll end up close to where we started.
So we should reserve that for dependencies which are used in a large number of tests, e.g. something like FactoryBot.</p>

<p>What about hooks? RSpec lets us run specific code for tests with a particular tag:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="no">RSpec</span><span class="p">.</span><span class="nf">configure</span> <span class="k">do</span> <span class="o">|</span><span class="n">config</span><span class="o">|</span>
  <span class="n">config</span><span class="p">.</span><span class="nf">before</span><span class="p">(</span><span class="ss">:db</span><span class="p">)</span> <span class="k">do</span>
    <span class="nb">require</span> <span class="s2">"some-library"</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>This can be useful, but the downside is that we’re adding a layer of indirection.
A developer reading the test would need to know what a particular tag represents.</p>

<p>The last approach is to be explicit about each test’s dependencies, for example:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">require</span> <span class="s2">"rails_helper"</span>
<span class="nb">require</span> <span class="s2">"some-library"</span>
</code></pre></div></div>

<p>While this might result in a little more typing, it’s an effective way to manage dependency loading.</p>

<h2 id="communicating-the-change">Communicating the Change</h2>

<p>If you’re working on a team, you’ll want to ensure that while the changeover is in progress, new tests are written using the new <code class="language-plaintext highlighter-rouge">rails_helper.rb</code>.
This is something you could also add to your project’s README or developer documentation so that others joining the team are also aware.
You could also use a custom RuboCop check.</p>

<h2 id="final-steps">Final Steps</h2>

<p>Eventually, you’ll have moved every test over to the new <code class="language-plaintext highlighter-rouge">rails_helper.rb</code>. You can now delete <code class="language-plaintext highlighter-rouge">legacy_helper.rb</code>. You may also discover there gems in your Gemfile which are no longer needed, and can be dropped. You may also be able to remove unused files from <code class="language-plaintext highlighter-rouge">spec/support</code> if they are no longer referenced.</p>]]></content><author><name></name></author><summary type="html"><![CDATA[Having a fast feedback cycle is critical element of successful test-driven development. You should be able to run a single test within one second.]]></summary></entry></feed>