<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Peter H. Boling</title>
    <description>The latest articles on DEV Community by Peter H. Boling (@galtzo).</description>
    <link>https://dev.to/galtzo</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F561326%2F9da2905c-8840-4f44-8d63-6a9e03065371.jpeg</url>
      <title>DEV Community: Peter H. Boling</title>
      <link>https://dev.to/galtzo</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/galtzo"/>
    <language>en</language>
    <item>
      <title>Why flag_shih_tzu is changing its default SQL for bit flags</title>
      <dc:creator>Peter H. Boling</dc:creator>
      <pubDate>Tue, 26 May 2026 22:22:00 +0000</pubDate>
      <link>https://dev.to/galtzo/why-flagshihtzu-is-changing-its-default-sql-for-bit-flags-41l9</link>
      <guid>https://dev.to/galtzo/why-flagshihtzu-is-changing-its-default-sql-for-bit-flags-41l9</guid>
      <description>&lt;p&gt;&lt;a href="https://github.com/galtzo-floss/flag_shih_tzu" rel="noopener noreferrer"&gt;flag_shih_tzu&lt;/a&gt; stores many boolean attributes in one integer column. Each boolean gets one bit 〰️ well that used to be true 〰️ &lt;a href="https://github.com/galtzo-floss/flag_shih_tzu/releases/tag/v1.0.0" rel="noopener noreferrer"&gt;v1.0.0&lt;/a&gt; supports multi-bit flags, ternary, or even more for enum flags! 〰️ anyways, not the point of this article, so let's keep going:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;User&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ApplicationRecord&lt;/span&gt;
  &lt;span class="kp"&gt;include&lt;/span&gt; &lt;span class="no"&gt;FlagShihTzu&lt;/span&gt;

  &lt;span class="n"&gt;has_flags&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="ss"&gt;:warpdrive&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="ss"&gt;:shields&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Historically, the gem's default SQL for querying one flag used an &lt;code&gt;IN()&lt;/code&gt; list.&lt;br&gt;
For &lt;code&gt;warpdrive&lt;/code&gt;, the condition looked like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;users&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;flags&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is correct while the application knows about exactly two flags. The&lt;br&gt;
possible values where bit 1 is enabled are &lt;code&gt;1&lt;/code&gt; and &lt;code&gt;3&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The problem appears when flags are added during a rolling deploy.&lt;/p&gt;
&lt;h2&gt;
  
  
  The deploy that breaks old queries
&lt;/h2&gt;

&lt;p&gt;Suppose the next version of the app adds a new flag:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;User&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ApplicationRecord&lt;/span&gt;
  &lt;span class="kp"&gt;include&lt;/span&gt; &lt;span class="no"&gt;FlagShihTzu&lt;/span&gt;

  &lt;span class="n"&gt;has_flags&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="ss"&gt;:warpdrive&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="ss"&gt;:shields&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="mi"&gt;3&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="ss"&gt;:premium&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In the same deploy, a migration or background job sets the new bit for existing&lt;br&gt;
rows:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt;
&lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;flags&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;flags&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="s1"&gt;'2026-01-01'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A user that previously had only &lt;code&gt;warpdrive&lt;/code&gt; moved from &lt;code&gt;flags = 1&lt;/code&gt; to&lt;br&gt;
&lt;code&gt;flags = 5&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;During a rolling deploy, old application processes may still be serving&lt;br&gt;
requests. Those old processes still think only two flags exist, so they still&lt;br&gt;
query &lt;code&gt;warpdrive&lt;/code&gt; with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;users&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;flags&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That query no longer returns the &lt;code&gt;flags = 5&lt;/code&gt; row, even though the &lt;code&gt;warpdrive&lt;/code&gt;&lt;br&gt;
bit is still set.&lt;/p&gt;

&lt;p&gt;That is the bug. Nothing is wrong with the row. The old query is too dependent&lt;br&gt;
on knowing every possible future flag combination.&lt;/p&gt;
&lt;h2&gt;
  
  
  Bit operators match the model
&lt;/h2&gt;

&lt;p&gt;The next major &lt;code&gt;flag_shih_tzu&lt;/code&gt; release changes the default query mode to&lt;br&gt;
&lt;code&gt;:bit_operator&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The same &lt;code&gt;warpdrive&lt;/code&gt; query becomes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;users&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;flags&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That condition asks the database the same question the application asks:&lt;br&gt;
"is this bit set?"&lt;/p&gt;

&lt;p&gt;It keeps working if the row is &lt;code&gt;1&lt;/code&gt;, &lt;code&gt;3&lt;/code&gt;, &lt;code&gt;5&lt;/code&gt;, &lt;code&gt;7&lt;/code&gt;, or any future value with&lt;br&gt;
the &lt;code&gt;warpdrive&lt;/code&gt; bit enabled.&lt;/p&gt;

&lt;p&gt;For negated scopes, the generated SQL becomes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;users&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;flags&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Chained flag conditions also use bit checks by default, so a query for&lt;br&gt;
&lt;code&gt;warpdrive&lt;/code&gt; and &lt;code&gt;shields&lt;/code&gt; becomes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;users&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;flags&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;flags&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  This is a breaking change
&lt;/h2&gt;

&lt;p&gt;This changes generated SQL, so it belongs in a major release.&lt;/p&gt;

&lt;p&gt;Most application code should not need to change. Calls like&lt;br&gt;
&lt;code&gt;User.warpdrive&lt;/code&gt;, &lt;code&gt;User.not_warpdrive&lt;/code&gt;, and &lt;code&gt;User.warpdrive_condition&lt;/code&gt; keep the&lt;br&gt;
same Ruby API. The SQL string and database query plan may change.&lt;/p&gt;

&lt;p&gt;If your application depends on the old SQL shape, you can opt back in globally:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="no"&gt;FlagShihTzu&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;default_flag_query_mode&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="ss"&gt;:in_list&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or per model declaration:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;has_flags&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="ss"&gt;:warpdrive&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="ss"&gt;:shields&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="ss"&gt;flag_query_mode: :in_list&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The old mode is still supported. It is just no longer the safest default.&lt;/p&gt;

&lt;h2&gt;
  
  
  Performance tradeoff
&lt;/h2&gt;

&lt;p&gt;An &lt;code&gt;IN()&lt;/code&gt; list can be faster for some databases and indexes when the set of&lt;br&gt;
flags is small and fixed. A bit operation may not use the same index strategy.&lt;/p&gt;

&lt;p&gt;That tradeoff is real. But defaults should protect correctness first.&lt;/p&gt;

&lt;p&gt;If your app has a fixed set of flags and you have measured that &lt;code&gt;IN()&lt;/code&gt; lists&lt;br&gt;
perform better for your workload, keep using &lt;code&gt;:in_list&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;If your app may add flags over time, especially during rolling deploys, the new&lt;br&gt;
default avoids a subtle class of production bugs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this matters
&lt;/h2&gt;

&lt;p&gt;Bit flags are attractive because they let applications add boolean features&lt;br&gt;
without changing table schemas. That benefit is only complete if the query&lt;br&gt;
strategy also tolerates new flags appearing while old app processes are still&lt;br&gt;
alive.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;flags &amp;amp; bit = bit&lt;/code&gt; does that. &lt;code&gt;flags in (known_combinations)&lt;/code&gt; does not.&lt;/p&gt;

&lt;p&gt;That is why &lt;code&gt;flag_shih_tzu&lt;/code&gt; is making &lt;code&gt;:bit_operator&lt;/code&gt; the default.&lt;/p&gt;

&lt;h2&gt;
  
  
  Support &amp;amp; Funding Info
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://opencollective.com/galtzo-floss#backer" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraster.shields.io%2Fopencollective%2Fbackers%2Fgaltzo-floss.png%3Fstyle%3Dfor-the-badge" alt="OpenCollective Backers" width="112" height="28"&gt;&lt;/a&gt; &lt;a href="https://opencollective.com/galtzo-floss#sponsor" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraster.shields.io%2Fopencollective%2Fsponsors%2Fgaltzo-floss.png%3Fstyle%3Dfor-the-badge" alt="OpenCollective Sponsors" width="122" height="28"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I am a full-time OSS maintainer. If you find &lt;a href="//github.com/pboling"&gt;my work&lt;/a&gt; valuable I ask that you become a sponsor. Every dollar helps!&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Support OSS Work &amp;amp;&lt;/th&gt;
&lt;th&gt;Access my "Sponsors" channel&lt;/th&gt;
&lt;th&gt;on Galtzo.com&lt;/th&gt;
&lt;th&gt;Discord 👇️ &lt;a href="https://discord.gg/3qme4XHNKN" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraster.shields.io%2Fdiscord%2F1373797679469170758%3Fstyle%3Dfor-the-badge" alt="Live Chat on Discord" width="144" height="28"&gt;&lt;/a&gt;
&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;a href="https://github.com/sponsors/pboling" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraster.shields.io%2Fbadge%2FSponsor_Me%21-pboling.png%3Fstyle%3Dsocial%26logo%3Dgithub" alt="Sponsor Me on Github" width="107" height="20"&gt;&lt;/a&gt; &lt;a href="https://liberapay.com/pboling/donate" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraster.shields.io%2Fliberapay%2Fgoal%2Fpboling.png%3Flogo%3Dliberapay%26color%3Da51611%26style%3Dflat" alt="Liberapay Goal Progress" width="131" height="20"&gt;&lt;/a&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;a href="https://www.buymeacoffee.com/pboling" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraster.shields.io%2Fbadge%2Fbuy_me_a_coffee-%25E2%259C%2593-a51611.png%3Fstyle%3Dflat" alt="Buy me a coffee" width="118" height="20"&gt;&lt;/a&gt; &lt;a href="https://ko-fi.com/O5O86SNP4" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraster.shields.io%2Fbadge%2Fko--fi-%25E2%259C%2593-a51611.png%3Fstyle%3Dflat" alt="Donate at ko-fi.com" width="54" height="20"&gt;&lt;/a&gt;
&lt;/td&gt;
&lt;td&gt;&lt;a href="https://www.paypal.com/paypalme/peterboling" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraster.shields.io%2Fbadge%2Fdonate-paypal-a51611.png%3Fstyle%3Dflat%26logo%3Dpaypal" alt="Donate on PayPal" width="111" height="20"&gt;&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href="https://polar.sh/pboling" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraster.shields.io%2Fbadge%2Fpolar-donate-a51611.png%3Fstyle%3Dflat" alt="Donate on Polar" width="84" height="20"&gt;&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

</description>
      <category>ruby</category>
      <category>rails</category>
      <category>activerecord</category>
      <category>bitfield</category>
    </item>
    <item>
      <title>yard-yaml 0.1.1: safer UTF-8 handling for YAML documentation</title>
      <dc:creator>Peter H. Boling</dc:creator>
      <pubDate>Mon, 25 May 2026 04:24:26 +0000</pubDate>
      <link>https://dev.to/galtzo/yard-yaml-011-safer-utf-8-handling-for-yaml-documentation-43jh</link>
      <guid>https://dev.to/galtzo/yard-yaml-011-safer-utf-8-handling-for-yaml-documentation-43jh</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkcq7m9s99meql13xxvxv.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkcq7m9s99meql13xxvxv.png" alt="yard-fence logo" width="192" height="192"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;yard-yaml&lt;/code&gt; 0.1.1 is available.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;yard-yaml&lt;/code&gt; is a RubyGem that plugs into &lt;a href="https://yardoc.org/" rel="noopener noreferrer"&gt;YARD&lt;/a&gt; and helps Ruby projects document YAML files alongside the rest of their API docs. It can discover &lt;code&gt;.yml&lt;/code&gt;, &lt;code&gt;.yaml&lt;/code&gt;, and Citation File Format (&lt;code&gt;.cff&lt;/code&gt;) files, convert them to HTML pages, and expose inline documentation tags such as &lt;code&gt;@yaml&lt;/code&gt; and &lt;code&gt;@yaml_file&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This release is mostly about making the converter more resilient around file encodings, plus keeping the generated project tooling current.&lt;/p&gt;

&lt;h2&gt;
  
  
  What changed
&lt;/h2&gt;

&lt;p&gt;The main user-facing fix is in &lt;code&gt;Yard::Yaml::Converter.from_file&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;In 0.1.1 it now:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;preserves valid UTF-8 text&lt;/li&gt;
&lt;li&gt;scrubs malformed UTF-8 safely in non-strict mode&lt;/li&gt;
&lt;li&gt;rejects binary-ish inputs cleanly instead of raising raw encoding crashes&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That matters for documentation pipelines because YAML-like files are often edited by different tools, copied between systems, or generated by automation. A documentation build should fail clearly when the input is not text, and it should handle recoverable encoding issues predictably when strict mode is disabled.&lt;/p&gt;

&lt;h2&gt;
  
  
  Documentation and CI maintenance
&lt;/h2&gt;

&lt;p&gt;This release also includes generated project maintenance from the current &lt;code&gt;kettle-jem&lt;/code&gt; template:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;refreshed generated project tooling&lt;/li&gt;
&lt;li&gt;refreshed CI support&lt;/li&gt;
&lt;li&gt;refreshed documentation support&lt;/li&gt;
&lt;li&gt;generated CI coverage for &lt;code&gt;rdoc&lt;/code&gt; &lt;code&gt;~&amp;gt; 6.11&lt;/code&gt; and &lt;code&gt;&amp;gt;= 7.0&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For contributors working across sibling repositories, 0.1.1 also adds &lt;code&gt;documentation_local.gemfile&lt;/code&gt; support under &lt;code&gt;KETTLE_RB_DEV&lt;/code&gt;, which makes local documentation development smoother in a multi-repo workspace.&lt;/p&gt;

&lt;h2&gt;
  
  
  Quick usage
&lt;/h2&gt;

&lt;p&gt;Install the gem:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bundle add yard-yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then enable the YARD plugin in &lt;code&gt;.yardopts&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;--plugin yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;From there, &lt;code&gt;yard-yaml&lt;/code&gt; can participate in &lt;code&gt;yard doc&lt;/code&gt; generation and convert YAML documents, including &lt;code&gt;.cff&lt;/code&gt; files, into documentation output.&lt;/p&gt;

&lt;p&gt;The plugin also supports inline docstring tags:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# @yaml&lt;/span&gt;
&lt;span class="c1"&gt;# ---&lt;/span&gt;
&lt;span class="c1"&gt;# title: Example YAML&lt;/span&gt;
&lt;span class="c1"&gt;# description: This is an example YAML block.&lt;/span&gt;
&lt;span class="c1"&gt;# ---&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And file references:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# @yaml_file path/to/example.yml&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Release links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Release: &lt;a href="https://github.com/galtzo-floss/yard-yaml/releases/tag/v0.1.1" rel="noopener noreferrer"&gt;https://github.com/galtzo-floss/yard-yaml/releases/tag/v0.1.1&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Changelog compare: &lt;a href="https://github.com/galtzo-floss/yard-yaml/compare/v0.1.0...v0.1.1" rel="noopener noreferrer"&gt;https://github.com/galtzo-floss/yard-yaml/compare/v0.1.0...v0.1.1&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;RubyDoc: &lt;a href="http://rubydoc.info/gems/yard-yaml" rel="noopener noreferrer"&gt;http://rubydoc.info/gems/yard-yaml&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Source: &lt;a href="https://github.com/galtzo-floss/yard-yaml" rel="noopener noreferrer"&gt;https://github.com/galtzo-floss/yard-yaml&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Thanks to everyone building Ruby documentation tooling and keeping project docs close to the code they describe.&lt;/p&gt;

</description>
      <category>ruby</category>
      <category>rubygems</category>
      <category>yard</category>
      <category>yaml</category>
    </item>
    <item>
      <title>yard-fence 0.9.0: cleaner YARD docs when Markdown braces get in the way</title>
      <dc:creator>Peter H. Boling</dc:creator>
      <pubDate>Sun, 24 May 2026 06:29:36 +0000</pubDate>
      <link>https://dev.to/galtzo/yard-fence-090-cleaner-yard-docs-when-markdown-braces-get-in-the-way-2683</link>
      <guid>https://dev.to/galtzo/yard-fence-090-cleaner-yard-docs-when-markdown-braces-get-in-the-way-2683</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqsdy2yt675r2scnxl1nm.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqsdy2yt675r2scnxl1nm.png" alt="yard-fence logo" width="192" height="192"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;🤺 &lt;code&gt;yard-fence&lt;/code&gt; 0.9.0 is out.&lt;/p&gt;

&lt;p&gt;This is the first blog post I have written for the gem, so I will start with the short version:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;yard-fence&lt;/code&gt; is a Ruby gem that helps YARD generate cleaner documentation from Markdown files that contain braces.&lt;/p&gt;

&lt;p&gt;If you have ever had README examples, inline code, or template placeholders like &lt;code&gt;{issuer}&lt;/code&gt; or &lt;code&gt;{{TOKEN}}&lt;/code&gt; cause noisy YARD &lt;code&gt;InvalidLink&lt;/code&gt; warnings, &lt;code&gt;yard-fence&lt;/code&gt; exists for that problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem
&lt;/h2&gt;

&lt;p&gt;YARD is great at generating Ruby API documentation, and it can include Markdown content like a README in the generated docs. The trouble starts when Markdown content contains brace-heavy examples.&lt;/p&gt;

&lt;p&gt;That can happen in a lot of normal documentation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;Use &lt;span class="sb"&gt;`{issuer}`&lt;/span&gt; as the issuer placeholder.

&lt;span class="p"&gt;```&lt;/span&gt;&lt;span class="nl"&gt;ruby
&lt;/span&gt;&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;headers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="s2"&gt;"Authorization"&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"Bearer {{TOKEN}}"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;```&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Those braces are ordinary text to the author, but YARD can interpret brace content as reference/link syntax. The result is documentation noise, usually in the form of &lt;code&gt;InvalidLink&lt;/code&gt; warnings.&lt;/p&gt;

&lt;p&gt;Ignoring those warnings is tempting, but it weakens the signal from the documentation build. Once a build always emits known warnings, new warnings are easier to miss.&lt;/p&gt;

&lt;h2&gt;
  
  
  What yard-fence does
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;yard-fence&lt;/code&gt; puts a small preprocessing fence around the Markdown files YARD reads.&lt;/p&gt;

&lt;p&gt;During the Rake-based YARD workflow, it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Copies top-level Markdown and text files into &lt;code&gt;tmp/yard-fence/&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Replaces ASCII braces inside fenced code blocks, inline code spans, and simple placeholders with visually similar fullwidth braces&lt;/li&gt;
&lt;li&gt;Points YARD at the staged files instead of the originals&lt;/li&gt;
&lt;li&gt;Lets YARD generate HTML without treating those braces as links&lt;/li&gt;
&lt;li&gt;Restores the generated HTML back to normal ASCII &lt;code&gt;{}&lt;/code&gt; braces afterward&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The important part: your generated docs still contain copy-pastable code examples.&lt;/p&gt;

&lt;p&gt;The conversion is temporary staging for YARD, not a change to your source files.&lt;/p&gt;

&lt;h2&gt;
  
  
  What changed in 0.9.0
&lt;/h2&gt;

&lt;p&gt;The main 0.9.0 change is that documentation processing is now explicitly Rake-driven.&lt;/p&gt;

&lt;p&gt;Projects should define their YARD task, then call:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="no"&gt;Yard&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Fence&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;install_rake_tasks!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:yard&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That wires &lt;code&gt;yard:fence:prepare&lt;/code&gt; before the selected YARD task and runs HTML post-processing after the YARD task completes.&lt;/p&gt;

&lt;p&gt;This release also removes global &lt;code&gt;at_exit&lt;/code&gt; post-processing. That is intentional. Raw &lt;code&gt;yard&lt;/code&gt; or &lt;code&gt;bin/yard&lt;/code&gt; does not run the full &lt;code&gt;yard-fence&lt;/code&gt; workflow anymore unless the caller invokes the Rake-integrated documentation task.&lt;/p&gt;

&lt;p&gt;The practical fix in 0.9.0: loading YARD during unrelated rake tasks no longer clears or rewrites &lt;code&gt;docs/&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Installation
&lt;/h2&gt;

&lt;p&gt;With Bundler:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;bundle add yard-fence
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or install the gem directly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;gem install yard-fence
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Basic usage
&lt;/h2&gt;

&lt;p&gt;Use the Rake integration so the prepare and postprocess steps run around the YARD build:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="nb"&gt;require&lt;/span&gt; &lt;span class="s2"&gt;"yard"&lt;/span&gt;
&lt;span class="nb"&gt;require&lt;/span&gt; &lt;span class="s2"&gt;"yard/fence"&lt;/span&gt;

&lt;span class="no"&gt;YARD&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Rake&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;YardocTask&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:yard&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;files&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="no"&gt;Yard&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Fence&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;install_rake_tasks!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:yard&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then build docs with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;bundle exec rake yard
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If your project exposes &lt;code&gt;bin/yard&lt;/code&gt;, treat it the same as raw &lt;code&gt;yard&lt;/code&gt;: it runs YARD itself, but it does not run the &lt;code&gt;yard-fence&lt;/code&gt; Rake integration.&lt;/p&gt;

&lt;h2&gt;
  
  
  Recommended .yardopts
&lt;/h2&gt;

&lt;p&gt;Point YARD at the staged Markdown/TXT files:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;--plugin fence
-e yard/fence/hoist.rb
--readme tmp/yard-fence/README.md
--charset utf-8
--markup markdown
--markup-provider kramdown
--output docs
'lib/**/*.rb'
-
'tmp/yard-fence/*.md'
'tmp/yard-fence/*.txt'
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This keeps YARD away from the unsanitized originals during the documentation build.&lt;/p&gt;

&lt;h2&gt;
  
  
  Environment variables
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;yard-fence&lt;/code&gt; has a few small controls:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Variable&lt;/th&gt;
&lt;th&gt;Default&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;YARD_FENCE_DISABLE&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;false&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Disable all &lt;code&gt;yard-fence&lt;/code&gt; processing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;YARD_FENCE_CLEAN_DOCS&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;false&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Clear &lt;code&gt;docs/&lt;/code&gt; before regeneration&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;YARD_DEBUG&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;false&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Enable debug output&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;For example, if Markdown files were removed and you want to avoid stale generated pages:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;YARD_FENCE_CLEAN_DOCS=true bundle exec rake yard
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;tmp/yard-fence/&lt;/code&gt; staging directory is always cleared automatically before regeneration.&lt;/p&gt;

&lt;h2&gt;
  
  
  Release links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Release: &lt;a href="https://github.com/galtzo-floss/yard-fence/releases/tag/v0.9.0" rel="noopener noreferrer"&gt;https://github.com/galtzo-floss/yard-fence/releases/tag/v0.9.0&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Source: &lt;a href="https://github.com/galtzo-floss/yard-fence" rel="noopener noreferrer"&gt;https://github.com/galtzo-floss/yard-fence&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Docs: &lt;a href="http://rubydoc.info/gems/yard-fence" rel="noopener noreferrer"&gt;http://rubydoc.info/gems/yard-fence&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;🤺 If your YARD docs have noisy brace-related &lt;code&gt;InvalidLink&lt;/code&gt; warnings, give &lt;code&gt;yard-fence&lt;/code&gt; a try.&lt;/p&gt;

</description>
      <category>ruby</category>
      <category>rubygems</category>
      <category>yard</category>
      <category>opensource</category>
    </item>
    <item>
      <title>yard-timekeeper: Stop YARD Timestamp Churn in Checked-In Docs</title>
      <dc:creator>Peter H. Boling</dc:creator>
      <pubDate>Sun, 24 May 2026 00:35:28 +0000</pubDate>
      <link>https://dev.to/galtzo/yard-timekeeper-stop-yard-timestamp-churn-in-checked-in-docs-369o</link>
      <guid>https://dev.to/galtzo/yard-timekeeper-stop-yard-timestamp-churn-in-checked-in-docs-369o</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7u1025t8dwdbzt4zgk6h.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7u1025t8dwdbzt4zgk6h.png" alt="yard-timekeeper logo" width="192" height="192"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;🕰️ I just released &lt;a href="https://github.com/galtzo-floss/yard-timekeeper" rel="noopener noreferrer"&gt;&lt;code&gt;yard-timekeeper&lt;/code&gt;&lt;/a&gt; v0.1.0, a small RubyGem for Ruby projects that check generated YARD HTML into git.&lt;/p&gt;

&lt;p&gt;It solves a very specific kind of documentation noise: timestamp-only churn.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem
&lt;/h2&gt;

&lt;p&gt;If your project publishes generated YARD documentation from a checked-in &lt;code&gt;docs/&lt;/code&gt; directory, you have probably seen this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;You run the docs task.&lt;/li&gt;
&lt;li&gt;YARD regenerates HTML.&lt;/li&gt;
&lt;li&gt;Git reports changed files.&lt;/li&gt;
&lt;li&gt;You inspect the diff.&lt;/li&gt;
&lt;li&gt;The only change is the footer timestamp.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That is not a documentation change. It is build noise.&lt;/p&gt;

&lt;p&gt;For projects that keep generated docs under version control, this creates unnecessary diffs, noisier pull requests, and extra review work. It also makes it harder to notice when documentation actually changed.&lt;/p&gt;

&lt;h2&gt;
  
  
  What &lt;code&gt;yard-timekeeper&lt;/code&gt; Does
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;yard-timekeeper&lt;/code&gt; runs after YARD generates HTML and checks tracked files under &lt;code&gt;docs/**/*.html&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;If a file's only change is the generated footer timestamp, it restores that file from git.&lt;/p&gt;

&lt;p&gt;If the page has real content changes, it leaves the file alone.&lt;/p&gt;

&lt;p&gt;The goal is not to hide documentation changes. The goal is to remove timestamp-only churn while preserving the signal that matters.&lt;/p&gt;

&lt;h2&gt;
  
  
  Install
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gem &lt;span class="nb"&gt;install &lt;/span&gt;yard-timekeeper
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or add it with Bundler:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bundle add yard-timekeeper
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Configuration
&lt;/h2&gt;

&lt;p&gt;The supported workflow is through &lt;code&gt;rake yard&lt;/code&gt;, so the post-process hook can run after YARD finishes.&lt;/p&gt;

&lt;p&gt;In your &lt;code&gt;Rakefile&lt;/code&gt; or documentation task setup:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="nb"&gt;require&lt;/span&gt; &lt;span class="s2"&gt;"yard/timekeeper"&lt;/span&gt;

&lt;span class="no"&gt;Yard&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Timekeeper&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;install_rake_tasks!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:yard&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then generate docs with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bin/rake yard
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No &lt;code&gt;.yardopts&lt;/code&gt; plugin entry is required for this integration. If you were experimenting with &lt;code&gt;--plugin timekeeper&lt;/code&gt;, remove it for this workflow and use the rake integration instead.&lt;/p&gt;

&lt;h2&gt;
  
  
  Behavior
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;yard-timekeeper&lt;/code&gt; is intentionally conservative:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It only post-processes &lt;code&gt;docs/**/*.html&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;It only restores files already tracked by git.&lt;/li&gt;
&lt;li&gt;It only restores files whose diff is timestamp-only.&lt;/li&gt;
&lt;li&gt;It preserves real generated documentation changes.&lt;/li&gt;
&lt;li&gt;It can be disabled with &lt;code&gt;YARD_TIMEKEEPER_DISABLE=true&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That means new documentation pages, deleted pages, and pages with real content edits remain visible to git.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why This Exists
&lt;/h2&gt;

&lt;p&gt;I maintain a lot of Ruby gems, and many of them publish YARD docs from checked-in HTML. I want documentation generation to be repeatable without filling commits with timestamp changes.&lt;/p&gt;

&lt;p&gt;Small tooling like this is not glamorous, but it improves the daily maintenance loop. Cleaner diffs mean easier review. Easier review means fewer mistakes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;GitHub: &lt;a href="https://github.com/galtzo-floss/yard-timekeeper" rel="noopener noreferrer"&gt;https://github.com/galtzo-floss/yard-timekeeper&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;RubyDoc: &lt;a href="https://www.rubydoc.info/gems/yard-timekeeper" rel="noopener noreferrer"&gt;https://www.rubydoc.info/gems/yard-timekeeper&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Docs: &lt;a href="https://yard-timekeeper.galtzo.com" rel="noopener noreferrer"&gt;https://yard-timekeeper.galtzo.com&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;yard-timekeeper&lt;/code&gt; v0.1.0 is available now.&lt;/p&gt;

&lt;p&gt;🕰️ May your docs be fresh, and your diffs quiet.&lt;/p&gt;

</description>
      <category>ruby</category>
      <category>rubygem</category>
      <category>yard</category>
      <category>opensource</category>
    </item>
    <item>
      <title>💎REL: oauth2 v2.0.18</title>
      <dc:creator>Peter H. Boling</dc:creator>
      <pubDate>Wed, 01 Apr 2026 05:59:42 +0000</pubDate>
      <link>https://dev.to/galtzo/rel-oauth2-v2018-43pf</link>
      <guid>https://dev.to/galtzo/rel-oauth2-v2018-43pf</guid>
      <description>&lt;p&gt;oauth2 v2.0.18 was released... &lt;a href="https://github.com/ruby-oauth/oauth2/releases/tag/v2.0.18" rel="noopener noreferrer"&gt;almost five months ago&lt;/a&gt;. And I never got around to posting about it. Being unemployed is a LOT of work...&lt;/p&gt;

&lt;p&gt;As a participant in &lt;a href="https://github.blog/open-source/maintainers/securing-the-ai-software-supply-chain-security-results-across-67-open-source-projects/" rel="noopener noreferrer"&gt;Session 3 of GitHub Secure Open Source Fund&lt;/a&gt; I was able to learn about implementing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;an incident response plan&lt;/li&gt;
&lt;li&gt;threat model analysis&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These are critical tools to have in place so you have a guide when things go awry. ;) Those are the main changes in v2.0.18.&lt;/p&gt;

&lt;p&gt;Another release is coming soon...&lt;/p&gt;

&lt;h1&gt;
  
  
  Ruby #GitHub #Security
&lt;/h1&gt;

&lt;p&gt;Cover Photo (Cropped) by &lt;a href="https://unsplash.com/@drskdr?utm_source=unsplash&amp;amp;utm_medium=referral&amp;amp;utm_content=creditCopyText" rel="noopener noreferrer"&gt;Dasha Yukhymyuk&lt;/a&gt; on &lt;a href="https://unsplash.com/photos/red-and-black-led-light-rPG1Fg8UDUw?utm_source=unsplash&amp;amp;utm_medium=referral&amp;amp;utm_content=creditCopyText" rel="noopener noreferrer"&gt;Unsplash&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ruby</category>
      <category>oauth</category>
      <category>security</category>
      <category>github</category>
    </item>
    <item>
      <title>🐠 ANN: appraisal2 v3.0.6 - support frozen appraisal lockfiles</title>
      <dc:creator>Peter H. Boling</dc:creator>
      <pubDate>Wed, 18 Feb 2026 06:20:33 +0000</pubDate>
      <link>https://dev.to/galtzo/ann-appraisal2-v306-support-frozen-appraisal-lockfiles-20ml</link>
      <guid>https://dev.to/galtzo/ann-appraisal2-v306-support-frozen-appraisal-lockfiles-20ml</guid>
      <description>&lt;p&gt;An issue was reported by Richard Kramer, and made me aware of a use case that I had never personally used, or even considered - which is committing the lockfiles for appraisals. I have always added &lt;code&gt;gemfiles/*.gemfile.lock&lt;/code&gt; to my .gitignore.&lt;/p&gt;

&lt;p&gt;In fact, when I had use cases where I wanted to run a CI workflow against a frozen lockfile I would avoid using appraisal2.  But no more. We now have first class support for frozen lockfiles, and the implied active bundler version switching that can happen at runtime.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/appraisal-rb/appraisal2/issues/21" rel="noopener noreferrer"&gt;Issue #21&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/appraisal-rb/appraisal2/pull/23" rel="noopener noreferrer"&gt;Pull #23&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;No breaking changes. No code changes needed for implementations. Just update, and go. Report back if anything breaks!&lt;/p&gt;

&lt;p&gt;I need your support. Please sponsor me here on &lt;a href="https://opencollective.com/appraisal-rb" rel="noopener noreferrer"&gt;OpenCollective&lt;/a&gt;, or on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/sponsors/pboling" rel="noopener noreferrer"&gt;@pboling on GitHub Sponsors&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://liberapay.com/pboling/donate" rel="noopener noreferrer"&gt;@pboling on Liberapay&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Thanks,&lt;br&gt;
@pboling&lt;/p&gt;

</description>
      <category>news</category>
      <category>ruby</category>
      <category>testing</category>
      <category>tooling</category>
    </item>
    <item>
      <title>Hostile Takeover of RubyGems: My Thoughts</title>
      <dc:creator>Peter H. Boling</dc:creator>
      <pubDate>Fri, 06 Feb 2026 01:00:32 +0000</pubDate>
      <link>https://dev.to/galtzo/hostile-takeover-of-rubygems-my-thoughts-5hlo</link>
      <guid>https://dev.to/galtzo/hostile-takeover-of-rubygems-my-thoughts-5hlo</guid>
      <description>&lt;p&gt;I'll keep this post evergreen, as the situation evolves. Also, when you are done reading - &lt;a href="https://galtzo.com" rel="noopener noreferrer"&gt;hire me&lt;/a&gt;.&lt;/p&gt;

&lt;h1&gt;
  
  
  👣🔍️ First some background reading 🕵️
&lt;/h1&gt;

&lt;ul&gt;
&lt;li&gt;RubyGems (the &lt;a href="https://github.com/rubygems/" rel="noopener noreferrer"&gt;GitHub org&lt;/a&gt;, not the website) &lt;a href="https://joel.drapper.me/p/ruby-central-security-measures/" rel="noopener noreferrer"&gt;suffered&lt;/a&gt; a &lt;a href="https://pup-e.com/blog/goodbye-rubygems/" rel="noopener noreferrer"&gt;hostile takeover&lt;/a&gt; in September 2025.&lt;/li&gt;
&lt;li&gt;Ultimately &lt;a href="https://www.reddit.com/r/ruby/s/gOk42POCaV" rel="noopener noreferrer"&gt;4 maintainers&lt;/a&gt; were &lt;a href="https://bsky.app/profile/martinemde.com/post/3m3occezxxs2q" rel="noopener noreferrer"&gt;hard removed&lt;/a&gt; and a (dubious) reason has been given for only 1 of those, while 2 others resigned in protest.&lt;/li&gt;
&lt;li&gt;It is a &lt;a href="https://joel.drapper.me/p/ruby-central-takeover/" rel="noopener noreferrer"&gt;complicated story&lt;/a&gt; which is difficult to &lt;a href="https://joel.drapper.me/p/ruby-central-fact-check/" rel="noopener noreferrer"&gt;parse quickly&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Simply put - there was active policy for adding or removing maintainers/owners of &lt;a href="https://github.com/ruby/rubygems/blob/b1ab33a3d52310a84d16b193991af07f5a6a07c0/doc/rubygems/POLICIES.md?plain=1#L187-L196" rel="noopener noreferrer"&gt;rubygems&lt;/a&gt; and &lt;a href="https://github.com/ruby/rubygems/blob/b1ab33a3d52310a84d16b193991af07f5a6a07c0/doc/bundler/playbooks/TEAM_CHANGES.md" rel="noopener noreferrer"&gt;bundler&lt;/a&gt;, and those &lt;a href="https://www.reddit.com/r/ruby/comments/1ove9vp/rubycentral_hates_this_one_fact/" rel="noopener noreferrer"&gt;policies were not followed&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;I'm adding a note linking to this post to all of my gems because I &lt;a href="https://joel.drapper.me/p/ruby-central/" rel="noopener noreferrer"&gt;don't condone theft&lt;/a&gt; of repositories or gems from their rightful owners.&lt;/li&gt;
&lt;li&gt;If a similar theft happened with my repos/gems, I'd hope some would stand up for me.&lt;/li&gt;
&lt;li&gt;Disenfranchised former-maintainers have started &lt;a href="https://gem.coop" rel="noopener noreferrer"&gt;gem.coop&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Once available I will publish there, or to my own server, exclusively; unless RubyCentral &amp;amp; Ruby Core make amends with the community.&lt;/li&gt;
&lt;li&gt;The &lt;a href="https://youtu.be/_H4qbtC5qzU?si=BvuBU90R2wAqD2E6" rel="noopener noreferrer"&gt;"Technology for Humans: Joel Draper"&lt;/a&gt; podcast episode by &lt;a href="https://reinteractive.com/ruby-on-rails" rel="noopener noreferrer"&gt;reinteractive&lt;/a&gt; is the most cogent summary I'm aware of.&lt;/li&gt;
&lt;li&gt;See &lt;a href="https://github.com/gem-coop/gem.coop/issues/12" rel="noopener noreferrer"&gt;here&lt;/a&gt;, &lt;a href="https://gem.coop" rel="noopener noreferrer"&gt;here&lt;/a&gt; and &lt;a href="https://martinemde.com/2025/10/05/announcing-gem-coop.html" rel="noopener noreferrer"&gt;here&lt;/a&gt; for more info on what comes next.&lt;/li&gt;
&lt;/ul&gt;

&lt;h1&gt;
  
  
  My thoughts
&lt;/h1&gt;

&lt;ol&gt;
&lt;li&gt;I no longer trust Ruby Central.&lt;/li&gt;
&lt;li&gt;I no longer trust certain members, but primarily HSBT, of the RubyGems core team.&lt;/li&gt;
&lt;li&gt;I no longer trust certain members, but primarily HSBT and Matz, of the Ruby core team.&lt;/li&gt;
&lt;/ol&gt;

&lt;blockquote&gt;
&lt;p&gt;Q: In what sense do I &lt;em&gt;not trust&lt;/em&gt; them?&lt;br&gt;
A: 📃 &lt;strong&gt;Governance&lt;/strong&gt; 📃&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;To be more specific, I no longer trust that they:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Hold people accountable for their actions according to written agreements and documentation around governance policy. &lt;/li&gt;
&lt;li&gt;Understand the community upset over point 1.&lt;/li&gt;
&lt;li&gt;Will ever do anything about it.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If they are added to your repository, you may wake up to find you have lost access to your own project.&lt;/p&gt;

&lt;p&gt;I'm not OK with this having already happened to others, and have taken steps to ensure it will not happen to me.&lt;/p&gt;

&lt;p&gt;Within my open source projects, I will reduce, to the degree possible, my reliance, on any project hosted under the Ruby org on GitHub. Since most of my projects are Ruby projects, I'll never get to complete exclusion, but I will be focusing much more on JRuby and Truffleruby.&lt;/p&gt;

&lt;p&gt;It has been pointed out to me in other discussions about this that we never had reason to trust them, but we did anyway, implicitly. We normally assume other people live by the same code of ethics that we ourselves live by. I will miss being able to rest on that assumption, but it is probably for the best that it get binned.&lt;/p&gt;

&lt;h1&gt;
  
  
  What I'm doing about it
&lt;/h1&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/appraisal-rb/appraisal2" rel="noopener noreferrer"&gt;appraisal2&lt;/a&gt; is a hard fork of the old, and nearly-dead, namesake Thoughtbot project, to which I've added many features, including support for:

&lt;ul&gt;
&lt;li&gt;Bundler's &lt;code&gt;eval_gemfile&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;frozen appraisal lockfiles w/ bundler version switching&lt;/li&gt;
&lt;li&gt;all versions of Ruby back to v1.8&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/contriboss/ore-light" rel="noopener noreferrer"&gt;ore&lt;/a&gt; (see below)&lt;/li&gt;
&lt;li&gt;More on the reasons behind the &lt;a href="https://dev.to/galtzo/ann-appraisal2-a-hard-fork-44dh"&gt;hard fork&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

&lt;a href="https://github.com/contriboss/ore-light" rel="noopener noreferrer"&gt;ore&lt;/a&gt; installs gems without Ruby, without bundler, and without rubygems. It is a GoLang implementation of (some parts of) Bundler (and adds some features bundler lacks). A project by &lt;a class="mentioned-user" href="https://dev.to/seuros"&gt;@seuros&lt;/a&gt; - and I'm now on the core team. It is &lt;em&gt;much&lt;/em&gt; faster than bundler.&lt;/li&gt;

&lt;li&gt;

&lt;a href="https://github.com/appraisal-rb/setup-ruby-flash" rel="noopener noreferrer"&gt;setup-ruby-flash&lt;/a&gt; is an alternative to the venerable setup-ruby GHA we've all been using for years. &lt;code&gt;setup-ruby-flash&lt;/code&gt; relies on &lt;a href="https://rv.dev/" rel="noopener noreferrer"&gt;rv&lt;/a&gt; and &lt;a href="https://github.com/contriboss/ore-light" rel="noopener noreferrer"&gt;ore&lt;/a&gt; for Ruby and Gem installs, and it falls back to &lt;a href="https://github.com/ruby/setup-ruby" rel="noopener noreferrer"&gt;setup-ruby&lt;/a&gt; on unsupported platforms/engines. I wrote more about it &lt;a href="https://dev.to/galtzo/setup-ruby-flash-25lb"&gt;here&lt;/a&gt;.&lt;/li&gt;

&lt;li&gt;A (WIP) proposal for &lt;a href="https://github.com/galtzo-floss/bundle-namespace" rel="noopener noreferrer"&gt;bundler/gem scopes&lt;/a&gt;
&lt;/li&gt;

&lt;li&gt;A (WIP) proposal for a federated &lt;a href="https://github.com/galtzo-floss/gem-server" rel="noopener noreferrer"&gt;gem server&lt;/a&gt;
&lt;/li&gt;

&lt;/ul&gt;

</description>
      <category>opensource</category>
      <category>ruby</category>
      <category>rails</category>
      <category>governance</category>
    </item>
    <item>
      <title>⚡️ setup-ruby-flash</title>
      <dc:creator>Peter H. Boling</dc:creator>
      <pubDate>Mon, 19 Jan 2026 03:03:27 +0000</pubDate>
      <link>https://dev.to/galtzo/setup-ruby-flash-25lb</link>
      <guid>https://dev.to/galtzo/setup-ruby-flash-25lb</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Find out how fast my workflows can go!&lt;br&gt;
-You, possibly&lt;/p&gt;
&lt;/blockquote&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;GH Marketplace&lt;/th&gt;
&lt;th&gt;Dogfood&lt;/th&gt;
&lt;th&gt;Current Tag&lt;/th&gt;
&lt;th&gt;License&lt;/th&gt;
&lt;th&gt;Source&lt;/th&gt;
&lt;th&gt;Stars&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://github.com/marketplace/actions/setup-ruby-with-rv-and-ore" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraster.shields.io%2Fbadge%2FMarketplace-238636%3F%26logo%3DGithub%26logoColor%3Dgreen" alt="GH Marketplace" width="95" height="20"&gt;&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href="https://github.com/appraisal-rb/setup-ruby-flash/actions/workflows/ci.yml" rel="noopener noreferrer"&gt;&lt;img src="https://github.com/appraisal-rb/setup-ruby-flash/actions/workflows/ci.yml/badge.svg" alt="CI" width="83" height="20"&gt;&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href="http://github.com/appraisal-rb/setup-ruby-flash/releases" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraster.shields.io%2Fgithub%2Ftag%2Fappraisal-rb%2Fsetup-ruby-flash.png" alt="GitHub tag (latest SemVer)" width="62" height="20"&gt;&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href="https://opensource.org/licenses/MIT" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraster.shields.io%2Fbadge%2FLicense-MIT-259D6C.png" alt="License: MIT" width="82" height="20"&gt;&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href="https://github.com/appraisal-rb/setup-ruby-flash" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraster.shields.io%2Fbadge%2FGitHub-238636%3F%26logo%3DGithub%26logoColor%3Dgreen" alt="GH Source" width="65" height="20"&gt;&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href="https://github.com/appraisal-rb/setup-ruby-flash/stargazers" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraster.shields.io%2Fgithub%2Fstars%2Fappraisal-rb%2Fsetup-ruby-flash.png" alt="GH Repo Stars" width="82" height="20"&gt;&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A &lt;em&gt;fast&lt;/em&gt; GitHub Action for fast Ruby environment setup using &lt;a href="https://github.com/spinel-coop/rv" rel="noopener noreferrer"&gt;rv&lt;/a&gt; for Ruby installation and &lt;a href="https://github.com/contriboss/ore-light" rel="noopener noreferrer"&gt;ore&lt;/a&gt; for gem management.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;⚡ Install Ruby in under 2 seconds&lt;/strong&gt; — no compilation required!&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;⚡ Install Gems 50% faster&lt;/strong&gt; — using ORE ✅️!&lt;/p&gt;

&lt;h2&gt;
  
  
  Features
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;🚀 &lt;strong&gt;Lightning-fast Ruby installation&lt;/strong&gt; via prebuilt binaries from rv&lt;/li&gt;
&lt;li&gt;📦 &lt;strong&gt;Rapid gem installation&lt;/strong&gt; with ore (Bundler-compatible, ~50% faster)&lt;/li&gt;
&lt;li&gt;💾 &lt;strong&gt;Intelligent caching&lt;/strong&gt; for both Ruby and gems&lt;/li&gt;
&lt;li&gt;🔒 &lt;strong&gt;Security auditing&lt;/strong&gt; via &lt;code&gt;ore audit&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;🐧 &lt;strong&gt;Linux &amp;amp; macOS support&lt;/strong&gt; (x86_64 and ARM64)&lt;/li&gt;
&lt;li&gt;☕️ &lt;strong&gt;Gitea &lt;a href="https://docs.gitea.com/usage/actions/overview" rel="noopener noreferrer"&gt;Actions&lt;/a&gt; support&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;🦊 &lt;strong&gt;Forgejo &lt;a href="https://forgejo.org/docs/next/admin/actions/" rel="noopener noreferrer"&gt;Actions&lt;/a&gt; support&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;🧊 &lt;strong&gt;Codeberg &lt;a href="https://docs.codeberg.org/ci/actions/" rel="noopener noreferrer"&gt;Actions&lt;/a&gt; support&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;🐙 &lt;strong&gt;GitHub &lt;a href="https://github.com/marketplace/actions/setup-ruby-with-rv-and-ore" rel="noopener noreferrer"&gt;Actions&lt;/a&gt; support&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Requirements
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Operating Systems&lt;/strong&gt;: Ubuntu 22.04+, macOS 14+&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Architectures&lt;/strong&gt;: x86_64, ARM64&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ruby Versions&lt;/strong&gt;: 3.2, 3.3, 3.4, 4.0&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;#&lt;/th&gt;
&lt;th&gt;Important&lt;/th&gt;
&lt;th&gt;Alternative&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Windows is not supported&lt;/td&gt;
&lt;td&gt;&lt;a href="https://github.com/ruby/setup-ruby" rel="noopener noreferrer"&gt;ruby/setup-ruby&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Ruby &amp;lt;= 3.1 is not supported&lt;/td&gt;
&lt;td&gt;&lt;a href="https://github.com/ruby/setup-ruby" rel="noopener noreferrer"&gt;ruby/setup-ruby&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Key Differences with setup-ruby
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;setup-ruby&lt;/th&gt;
&lt;th&gt;setup-ruby-flash&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Ruby Install&lt;/td&gt;
&lt;td&gt;~5 seconds&lt;/td&gt;
&lt;td&gt;&amp;lt; 2 seconds&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gem Install&lt;/td&gt;
&lt;td&gt;Bundler&lt;/td&gt;
&lt;td&gt;ore (~50% faster)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ruby-version: ruby&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;✅ latest stable&lt;/td&gt;
&lt;td&gt;✅ latest stable&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;rubygems: latest&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;bundler: latest&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Windows&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ruby &amp;lt; 3.2&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;JRuby&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;❌ (planned)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TruffleRuby&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;❌ (planned)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Security Audit&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;✅ (&lt;code&gt;ore audit&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Basic Usage
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;appraisal-rb/setup-ruby-flash@v1&lt;/span&gt;
  &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;ruby-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;3.4'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  With Gem Installation
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;appraisal-rb/setup-ruby-flash@v1&lt;/span&gt;
  &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;ruby-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;3.4'&lt;/span&gt;
    &lt;span class="na"&gt;ore-install&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Using Version Files
&lt;/h3&gt;

&lt;p&gt;When &lt;code&gt;ruby-version&lt;/code&gt; is set to &lt;code&gt;default&lt;/code&gt; (the default), setup-ruby-flash reads from:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;.ruby-version&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;.tool-versions&lt;/code&gt; (asdf format)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;mise.toml&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;appraisal-rb/setup-ruby-flash@v1&lt;/span&gt;
  &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;ore-install&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Enough Talk, Where?
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/marketplace/actions/setup-ruby-with-rv-and-ore" rel="noopener noreferrer"&gt;GitHub Marketplace&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/appraisal-rb/setup-ruby-flash" rel="noopener noreferrer"&gt;Source&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://opencollective.com/appraisal-rb" rel="noopener noreferrer"&gt;Support the Project&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Inputs
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Input&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;th&gt;Default&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ruby-version&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Ruby version to install (e.g., &lt;code&gt;3.4&lt;/code&gt;, &lt;code&gt;3.4.1&lt;/code&gt;). Use &lt;code&gt;ruby&lt;/code&gt; for latest stable version, or &lt;code&gt;default&lt;/code&gt; to read from version files.&lt;/td&gt;
&lt;td&gt;&lt;code&gt;default&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;rubygems&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;RubyGems version: &lt;code&gt;default&lt;/code&gt;, &lt;code&gt;latest&lt;/code&gt;, or a version number (e.g., &lt;code&gt;3.5.0&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;default&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;bundler&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Bundler version: &lt;code&gt;Gemfile.lock&lt;/code&gt;, &lt;code&gt;default&lt;/code&gt;, &lt;code&gt;latest&lt;/code&gt;, &lt;code&gt;none&lt;/code&gt;, or a version number&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Gemfile.lock&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ore-install&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Run &lt;code&gt;ore install&lt;/code&gt; and cache gems&lt;/td&gt;
&lt;td&gt;&lt;code&gt;false&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;working-directory&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Directory for version files and Gemfile&lt;/td&gt;
&lt;td&gt;&lt;code&gt;.&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;cache-version&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Cache version string for invalidation&lt;/td&gt;
&lt;td&gt;&lt;code&gt;v1&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;rv-version&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Version of rv to install (ignored if &lt;code&gt;rv-git-ref&lt;/code&gt; is set)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;latest&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;rv-git-ref&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Git branch, tag, or commit SHA to build rv from source&lt;/td&gt;
&lt;td&gt;&lt;code&gt;''&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ore-version&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Version of ore to install (ignored if &lt;code&gt;ore-git-ref&lt;/code&gt; is set)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;latest&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ore-git-ref&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Git branch, tag, or commit SHA to build ore from source&lt;/td&gt;
&lt;td&gt;&lt;code&gt;''&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;skip-extensions&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Skip building native extensions&lt;/td&gt;
&lt;td&gt;&lt;code&gt;false&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;without-groups&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Gem groups to exclude (comma-separated)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;''&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ruby-install-retries&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Number of retry attempts for Ruby installation (with exponential backoff)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;3&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;no-document&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Skip generating documentation (ri/rdoc) for installed gems. Creates &lt;code&gt;~/.gemrc&lt;/code&gt; with &lt;code&gt;gem: --no-document&lt;/code&gt; if file doesn't exist&lt;/td&gt;
&lt;td&gt;&lt;code&gt;true&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;token&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;GitHub token for API calls&lt;/td&gt;
&lt;td&gt;&lt;code&gt;${{ github.token }}&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Outputs
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Output&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ruby-version&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The installed Ruby version&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ruby-prefix&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The path to the Ruby installation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;rv-version&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The installed rv version&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;rubygems-version&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The installed RubyGems version&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;bundler-version&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The installed Bundler version&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ore-version&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The installed ore version&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;cache-hit&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Whether gems were restored from cache&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Examples
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Matrix Build
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;CI&lt;/span&gt;
&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;pull_request&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;test&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;strategy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;fail-fast&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
      &lt;span class="na"&gt;matrix&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;os&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;ubuntu-latest&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;macos-latest&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
        &lt;span class="na"&gt;ruby&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;3.2"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;3.3"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;3.4"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;4.0"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ matrix.os }}&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v5&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;appraisal-rb/setup-ruby-flash@v1&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;ruby-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ matrix.ruby }}&lt;/span&gt;
          &lt;span class="na"&gt;ore-install&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;bundle exec rake test&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Production Gems Only
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;appraisal-rb/setup-ruby-flash@v1&lt;/span&gt;
  &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;ruby-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;3.4'&lt;/span&gt;
    &lt;span class="na"&gt;ore-install&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="na"&gt;without-groups&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;development,test'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Latest Ruby with Latest RubyGems and Bundler
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;appraisal-rb/setup-ruby-flash@v1&lt;/span&gt;
  &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;ruby-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ruby&lt;/span&gt;
    &lt;span class="na"&gt;rubygems&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;latest&lt;/span&gt;
    &lt;span class="na"&gt;bundler&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;latest&lt;/span&gt;
    &lt;span class="na"&gt;ore-install&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Specific RubyGems Version
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;appraisal-rb/setup-ruby-flash@v1&lt;/span&gt;
  &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;ruby-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;3.4'&lt;/span&gt;
    &lt;span class="na"&gt;rubygems&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;3.5.0'&lt;/span&gt;
    &lt;span class="na"&gt;ore-install&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Skip Native Extensions
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;appraisal-rb/setup-ruby-flash@v1&lt;/span&gt;
  &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;ruby-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;3.4'&lt;/span&gt;
    &lt;span class="na"&gt;ore-install&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="na"&gt;skip-extensions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Custom Working Directory
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;appraisal-rb/setup-ruby-flash@v1&lt;/span&gt;
  &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;ruby-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;3.4'&lt;/span&gt;
    &lt;span class="na"&gt;ore-install&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="na"&gt;working-directory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;./my-app'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Specific Tool Versions
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;appraisal-rb/setup-ruby-flash@v1&lt;/span&gt;
  &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;ruby-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;3.4.1'&lt;/span&gt;
    &lt;span class="na"&gt;rv-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;0.4.0'&lt;/span&gt;
    &lt;span class="na"&gt;ore-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;0.1.0'&lt;/span&gt;
    &lt;span class="na"&gt;ore-install&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Custom Retry Configuration
&lt;/h3&gt;

&lt;p&gt;If you experience intermittent failures due to GitHub API rate limiting, you can adjust the number of retry attempts:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;appraisal-rb/setup-ruby-flash@v1&lt;/span&gt;
  &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;ruby-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;3.4'&lt;/span&gt;
    &lt;span class="na"&gt;ruby-install-retries&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;5'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Enable Documentation Generation
&lt;/h3&gt;

&lt;p&gt;Include documentation (ri/rdoc) for installed gems (default skips documentation for faster installation):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;appraisal-rb/setup-ruby-flash@v1&lt;/span&gt;
  &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;ruby-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;3.4"&lt;/span&gt;
    &lt;span class="na"&gt;ore-install&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="na"&gt;no-document&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Building rv or ore from Source
&lt;/h3&gt;

&lt;p&gt;You can build rv or ore from a git branch, tag, or commit SHA instead of using a released version.&lt;br&gt;
This is useful for testing unreleased features or bug fixes. Required toolchains (Rust for rv, Go for ore)&lt;br&gt;
are automatically installed. Fork syntax (&lt;code&gt;pboling:feat/myexperiment&lt;/code&gt;) is supported to test out feature branches in forks of ore or rv.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Test an ore feature branch&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;appraisal-rb/setup-ruby-flash@v1&lt;/span&gt;
  &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;ruby-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;3.4"&lt;/span&gt;
    &lt;span class="na"&gt;ore-install&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="na"&gt;ore-git-ref&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;feat/bundle-gemfile-support"&lt;/span&gt;

&lt;span class="c1"&gt;# Test a pre-release rv tag&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;appraisal-rb/setup-ruby-flash@v1&lt;/span&gt;
  &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;ruby-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;3.4"&lt;/span&gt;
    &lt;span class="na"&gt;rv-git-ref&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;v0.5.0-beta1"&lt;/span&gt;

&lt;span class="c1"&gt;# Test both from main branches&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;appraisal-rb/setup-ruby-flash@v1&lt;/span&gt;
  &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;ruby-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;3.4"&lt;/span&gt;
    &lt;span class="na"&gt;rv-git-ref&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;main"&lt;/span&gt;
    &lt;span class="na"&gt;ore-install&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="na"&gt;ore-git-ref&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;main"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Migration from setup-ruby
&lt;/h2&gt;

&lt;p&gt;setup-ruby-flash is designed to be a near drop-in replacement for &lt;code&gt;ruby/setup-ruby&lt;/code&gt; on supported platforms:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Before (setup-ruby)&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ruby/setup-ruby@v1&lt;/span&gt;
  &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;ruby-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;3.4'&lt;/span&gt;
    &lt;span class="na"&gt;bundler-cache&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;bundle exec rake test&lt;/span&gt;

&lt;span class="c1"&gt;# After (setup-ruby-flash)&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;appraisal-rb/setup-ruby-flash@v1&lt;/span&gt;
  &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;ruby-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;3.4'&lt;/span&gt;
    &lt;span class="na"&gt;ore-install&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;bundle exec rake test&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  With Latest RubyGems and Bundler
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Before (setup-ruby)&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ruby/setup-ruby@v1&lt;/span&gt;
  &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;ruby-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ruby&lt;/span&gt;
    &lt;span class="na"&gt;rubygems&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;latest&lt;/span&gt;
    &lt;span class="na"&gt;bundler&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;latest&lt;/span&gt;

&lt;span class="c1"&gt;# After (setup-ruby-flash)&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;appraisal-rb/setup-ruby-flash@v1&lt;/span&gt;
  &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;ruby-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ruby&lt;/span&gt;
    &lt;span class="na"&gt;rubygems&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;latest&lt;/span&gt;
    &lt;span class="na"&gt;bundler&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;latest&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Acknowledgements
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/ruby/setup-ruby" rel="noopener noreferrer"&gt;setup-ruby&lt;/a&gt; the venerable mainstay for many years, and inspiration for this project.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/spinel-coop/rv" rel="noopener noreferrer"&gt;rv&lt;/a&gt; by Spinel Cooperative&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/contriboss/ore-light" rel="noopener noreferrer"&gt;ore&lt;/a&gt; by Contriboss&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Support &amp;amp; Funding Info
&lt;/h2&gt;

&lt;p&gt;I am a full-time FLOSS maintainer. If you find &lt;a href="//github.com/pboling"&gt;my work&lt;/a&gt; valuable I ask that you become a sponsor. Every dollar helps!&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;🥰 Support FLOSS work 🥰&lt;/th&gt;
&lt;th&gt;Get access&lt;/th&gt;
&lt;th&gt;"Sponsors" channel&lt;/th&gt;
&lt;th&gt;on Galtzo FLOSS&lt;/th&gt;
&lt;th&gt;Discord 👇️ &lt;a href="https://discord.gg/3qme4XHNKN" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraster.shields.io%2Fdiscord%2F1373797679469170758%3Fstyle%3Dfor-the-badge" alt="Live Chat on Discord" width="144" height="28"&gt;&lt;/a&gt;
&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;a href="https://opencollective.com/appraisal-rb#backer" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraster.shields.io%2Fopencollective%2Fbackers%2Fappraisal-rb.png%3Fstyle%3Dfor-the-badge" alt="OpenCollective Backers" width="112" height="28"&gt;&lt;/a&gt; &lt;a href="https://opencollective.com/appraisal-rb#sponsor" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraster.shields.io%2Fopencollective%2Fsponsors%2Fappraisal-rb.png%3Fstyle%3Dfor-the-badge" alt="OpenCollective Sponsors" width="122" height="28"&gt;&lt;/a&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;a href="https://www.buymeacoffee.com/pboling" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraster.shields.io%2Fbadge%2Fbuy_me_a_coffee-%25E2%259C%2593-a51611.png%3Fstyle%3Dflat" alt="Buy me a coffee" width="118" height="20"&gt;&lt;/a&gt; &lt;a href="https://ko-fi.com/O5O86SNP4" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraster.shields.io%2Fbadge%2Fko--fi-%25E2%259C%2593-a51611.png%3Fstyle%3Dflat" alt="Donate at ko-fi.com" width="54" height="20"&gt;&lt;/a&gt;
&lt;/td&gt;
&lt;td&gt;&lt;a href="https://www.paypal.com/paypalme/peterboling" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraster.shields.io%2Fbadge%2Fdonate-paypal-a51611.png%3Fstyle%3Dflat%26logo%3Dpaypal" alt="Donate on PayPal" width="111" height="20"&gt;&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href="https://polar.sh/pboling" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraster.shields.io%2Fbadge%2Fpolar-donate-a51611.png%3Fstyle%3Dflat" alt="Donate on Polar" width="84" height="20"&gt;&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;
&lt;a href="https://github.com/sponsors/pboling" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraster.shields.io%2Fbadge%2FSponsor_Me%21-pboling.png%3Fstyle%3Dsocial%26logo%3Dgithub" alt="Sponsor Me on Github" width="107" height="20"&gt;&lt;/a&gt; &lt;a href="https://liberapay.com/pboling/donate" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraster.shields.io%2Fliberapay%2Fgoal%2Fpboling.png%3Flogo%3Dliberapay%26color%3Da51611%26style%3Dflat" alt="Liberapay Goal Progress" width="131" height="20"&gt;&lt;/a&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  About rv and ore
&lt;/h2&gt;

&lt;h3&gt;
  
  
  rv
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://github.com/spinel-coop/rv" rel="noopener noreferrer"&gt;rv&lt;/a&gt; is an extremely fast Ruby version manager written in Rust. It downloads prebuilt Ruby binaries, eliminating the need for compilation. Created by &lt;a href="https://github.com/indirect" rel="noopener noreferrer"&gt;@indirect&lt;/a&gt;, long-time project lead for Bundler and RubyGems.&lt;/p&gt;

&lt;h3&gt;
  
  
  ore
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://github.com/contriboss/ore-light" rel="noopener noreferrer"&gt;ore&lt;/a&gt; is a fast gem installer written in Go. It's Bundler-compatible but performs downloads significantly faster using Go's concurrency features. Use &lt;code&gt;bundle exec&lt;/code&gt; to run gem commands after ore installs your gems. Created by &lt;a href="https://github.com/seuros" rel="noopener noreferrer"&gt;@seuros&lt;/a&gt;, a long time Rubyist, and prolific &lt;a href="https://www.seuros.com/blog/rubygems-coup-when-parasites-take-the-host/" rel="noopener noreferrer"&gt;writer&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Cover photo (cropped) by &lt;a href="https://unsplash.com/@whereiskylenow?utm_source=unsplash&amp;amp;utm_medium=referral&amp;amp;utm_content=creditCopyText" rel="noopener noreferrer"&gt;Kyle Hinkson&lt;/a&gt; on &lt;a href="https://unsplash.com/photos/streetcar-passing-through-modern-city-buildings-with-cn-tower--NLQW5yTKYo?utm_source=unsplash&amp;amp;utm_medium=referral&amp;amp;utm_content=creditCopyText" rel="noopener noreferrer"&gt;Unsplash&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ruby</category>
      <category>gha</category>
      <category>ore</category>
      <category>rv</category>
    </item>
    <item>
      <title>💲FLOSS Funding</title>
      <dc:creator>Peter H. Boling</dc:creator>
      <pubDate>Thu, 06 Nov 2025 23:39:10 +0000</pubDate>
      <link>https://dev.to/galtzo/floss-funding-2cfn</link>
      <guid>https://dev.to/galtzo/floss-funding-2cfn</guid>
      <description>&lt;h1&gt;
  
  
  Two things are true.
&lt;/h1&gt;

&lt;p&gt;Many open source contributors have been laid off recently, and have not been able to find new roles. Many of these have become the fabled "person in Nebraska", made famous by XKCD #2347.&lt;br&gt;
Every RubyGem, NPM, pip, cargo, etc, package you have ever used is worth at least $1 to you and the careers and products they helped build. Many people would contribute more financially to open source if more systems were in place to facilitate that.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://xkcd.com/2347/" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fh6a6f560xn8dvmhcpu1o.png" alt="All modern digital infrastructure (depending on) a project some random person in Nebraska has been thanklessly maintaining since 2003" width="385" height="489"&gt;&lt;/a&gt;&lt;br&gt;
﻿&lt;/p&gt;

&lt;p&gt;﻿&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;All modern digital infrastructure (depending on) a project some random person in Nebraska has been thanklessly maintaining since 2003 - &lt;a href="https://xkcd.com/2347/" rel="noopener noreferrer"&gt;xkcd.com/2347&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h1&gt;
  
  
  You may be saying:
&lt;/h1&gt;

&lt;blockquote&gt;
&lt;p&gt;Systems do exist to enable donations or subscriptions to developers.  In fact, I can name several!&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Yes, fine, three things are true. Those systems are excellent, and underutilized. Why?&lt;/p&gt;

&lt;p&gt;Finding the funding path for a project is hard. Unless you are the meticulous type to memorize a Gemfile.lock, yarn.lock, or package-lock.json, many, if not most, of the projects at the bottom of the tree get overlooked. Since they know they will be overlooked, they often don't even bother setting up paths for donations.&lt;/p&gt;

&lt;h1&gt;
  
  
  What kinds of dependencies?
&lt;/h1&gt;

&lt;p&gt;The nested, indirect, dependencies loaded by your direct dependencies.&lt;br&gt;
The development and test dependencies which you rely on for AI to validate its work when you are vibe coding&lt;br&gt;
The ones that take just as much effort to build, but fail to get recognized as valuable.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Aren't there tools that automatically dig into the dependency tree to help fund those indirect dependencies?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Yes, those are great, and I encourage people to use them. So apparently four things are true, but:&lt;/p&gt;

&lt;p&gt;None dig down more than 3 levels into a dependency tree, and trees can be much deeper than that. In Ruby, for example:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;bundle fund&lt;/code&gt; doesn't consider indirect dependencies 

&lt;ul&gt;
&lt;li&gt;In some contexts they only consider runtime dependencies&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;
&lt;code&gt;bundle fund&lt;/code&gt; doesn't consider development dependencies in Gemfile during RubyGem development&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;I love &lt;code&gt;bundle fund&lt;/code&gt;, but if the command isn't run, people don't see the requests for help with funding, and it is never run automatically.  I expect there are similar issues with &lt;code&gt;npm fund&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Currently, funding for the majority of free (libre) open source code (FLOSS) is hard.&lt;/p&gt;

&lt;h1&gt;
  
  
  What if it was easy?
&lt;/h1&gt;

&lt;blockquote&gt;
&lt;p&gt;What if a project could tell you how to support it precisely when you are using it, and deriving value from it?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;New #Ruby gem to fund open source developers whose projects get missed by other #FLOSS #funding tools which don’t cover dev deps, nor indirect deps &amp;gt; 3 levels down.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://ruby.social/@galtzo/114994584740434327" rel="noopener noreferrer"&gt;https://ruby.social/@galtzo/114994584740434327&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;👉 no tracking&lt;br&gt;
👉 no network calls&lt;br&gt;
👉 never forces or requires payment&lt;br&gt;
👉 supports buy-once, or per-version &lt;br&gt;
👉 easily silenced nags &lt;br&gt;
👉 sets of related libraries share activation keys &lt;br&gt;
👉 nags are opt-in for maintainers, opt-out for users&lt;br&gt;
👉 nags are throttled to customizable frequency&lt;br&gt;
👉 inert in non-TTY shells (i.e. production-like environments)&lt;br&gt;
👉 inert when exit status is non-zero&lt;br&gt;
👉 inert for many edge cases, such as an internal failure&lt;br&gt;
👉 inert when a global silencing ENV variable is set (you're welcome, haters!)&lt;/p&gt;

&lt;p&gt;Open source projects in which languages will have this ability?&lt;br&gt;
&lt;a href="https://github.com/floss-funding/floss_funding" rel="noopener noreferrer"&gt;Ruby&lt;/a&gt;&lt;br&gt;
Javascript&lt;br&gt;
Elixir&lt;br&gt;
Python&lt;br&gt;
Perl&lt;br&gt;
... Join the &lt;a href="https://discord.gg/3qme4XHNKN" rel="noopener noreferrer"&gt;discord&lt;/a&gt; if you have more ideas, and want to contribute. A lanugage just needs to have an "atexit()" style hook, i.e. a process termination hook. A bunch of languages meet the criteria, even Bash.&lt;/p&gt;

&lt;p&gt;Cover photo by me, CC-SA 4.0.&lt;/p&gt;

</description>
      <category>career</category>
      <category>discuss</category>
      <category>opensource</category>
    </item>
    <item>
      <title>💎 ANN: omniauth-identity v3.1.5 (Hanami/ROM Support)</title>
      <dc:creator>Peter H. Boling</dc:creator>
      <pubDate>Tue, 14 Oct 2025 00:49:55 +0000</pubDate>
      <link>https://dev.to/galtzo/ann-omniauth-identity-v315-hanamirom-support-4ha3</link>
      <guid>https://dev.to/galtzo/ann-omniauth-identity-v315-hanamirom-support-4ha3</guid>
      <description>&lt;h2&gt;
  
  
  &lt;a href="https://github.com/omniauth/omniauth-identity/compare/v3.1.4...v3.1.5" rel="noopener noreferrer"&gt;3.1.5&lt;/a&gt; - 2025-10-13
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;TAG: &lt;a href="https://github.com/omniauth/omniauth-identity/releases/tag/v3.1.5" rel="noopener noreferrer"&gt;v3.1.5&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;COVERAGE: 93.58% -- 437/467 lines in 14 files&lt;/li&gt;
&lt;li&gt;BRANCH COVERAGE: 81.00% -- 81/100 branches in 14 files&lt;/li&gt;
&lt;li&gt;92.39% documented&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Added
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Adapter support for Hanami and ROM&lt;/li&gt;
&lt;li&gt;Complete YARD documentation&lt;/li&gt;
&lt;li&gt;kettle-dev for easier maintenance &amp;amp; dev tooling&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The one where omniauth-identity gains a &lt;code&gt;rom&lt;/code&gt; adapter. Since &lt;code&gt;rom&lt;/code&gt; is the default DB adapter for Hanami, this also makes it &lt;em&gt;hanami-ready&lt;/em&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Identity&lt;/span&gt;
  &lt;span class="kp"&gt;include&lt;/span&gt; &lt;span class="no"&gt;OmniAuth&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Identity&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Models&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Rom&lt;/span&gt;

  &lt;span class="c1"&gt;# Configure the ROM container and relation using the DSL (no `self.`):&lt;/span&gt;
  &lt;span class="n"&gt;rom_container&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="no"&gt;MyDatabase&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;rom&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="c1"&gt;# accepts a proc or a container object&lt;/span&gt;
  &lt;span class="n"&gt;rom_relation_name&lt;/span&gt; &lt;span class="ss"&gt;:identities&lt;/span&gt; &lt;span class="c1"&gt;# optional, defaults to :identities&lt;/span&gt;
  &lt;span class="n"&gt;owner_relation_name&lt;/span&gt; &lt;span class="ss"&gt;:owners&lt;/span&gt;  &lt;span class="c1"&gt;# optional, for loading associated owner&lt;/span&gt;
  &lt;span class="c1"&gt;# Uses OmniAuth::Identity::Model.auth_key to set the auth key (defaults to :email)&lt;/span&gt;
  &lt;span class="n"&gt;auth_key&lt;/span&gt; &lt;span class="ss"&gt;:email&lt;/span&gt;
  &lt;span class="c1"&gt;# Optional: override the password digest field name (defaults to :password_digest)&lt;/span&gt;
  &lt;span class="n"&gt;password_field&lt;/span&gt; &lt;span class="ss"&gt;:password_digest&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Find a complete example after the funding info.&lt;/p&gt;

&lt;h2&gt;
  
  
  Support &amp;amp; Funding Info
&lt;/h2&gt;

&lt;p&gt;I am a full-time FLOSS maintainer. If you find &lt;a href="//github.com/pboling"&gt;my work&lt;/a&gt; valuable I ask that you become a sponsor. Every dollar helps!&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;🥰 Support FLOSS work 🥰&lt;/th&gt;
&lt;th&gt;Get access to "Sponsors" channel&lt;/th&gt;
&lt;th&gt;on Galtzo FLOSS&lt;/th&gt;
&lt;th&gt;Discord 👇️ &lt;a href="https://discord.gg/3qme4XHNKN" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraster.shields.io%2Fdiscord%2F1373797679469170758%3Fstyle%3Dfor-the-badge" alt="Live Chat on Discord" width="144" height="28"&gt;&lt;/a&gt;
&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;a href="https://github.com/sponsors/pboling" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraster.shields.io%2Fbadge%2FSponsor_Me%21-pboling.png%3Fstyle%3Dsocial%26logo%3Dgithub" alt="Sponsor Me on Github" width="107" height="20"&gt;&lt;/a&gt; &lt;a href="https://liberapay.com/pboling/donate" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraster.shields.io%2Fliberapay%2Fgoal%2Fpboling.png%3Flogo%3Dliberapay%26color%3Da51611%26style%3Dflat" alt="Liberapay Goal Progress" width="131" height="20"&gt;&lt;/a&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;a href="https://www.buymeacoffee.com/pboling" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraster.shields.io%2Fbadge%2Fbuy_me_a_coffee-%25E2%259C%2593-a51611.png%3Fstyle%3Dflat" alt="Buy me a coffee" width="118" height="20"&gt;&lt;/a&gt; &lt;a href="https://ko-fi.com/O5O86SNP4" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraster.shields.io%2Fbadge%2Fko--fi-%25E2%259C%2593-a51611.png%3Fstyle%3Dflat" alt="Donate at ko-fi.com" width="54" height="20"&gt;&lt;/a&gt;
&lt;/td&gt;
&lt;td&gt;&lt;a href="https://www.paypal.com/paypalme/peterboling" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraster.shields.io%2Fbadge%2Fdonate-paypal-a51611.png%3Fstyle%3Dflat%26logo%3Dpaypal" alt="Donate on PayPal" width="111" height="20"&gt;&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href="https://polar.sh/pboling" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraster.shields.io%2Fbadge%2Fpolar-donate-a51611.png%3Fstyle%3Dflat" alt="Donate on Polar" width="84" height="20"&gt;&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;As an example I'll use the code straight from the specs.&lt;/p&gt;

&lt;h1&gt;
  
  
  Test Harness
&lt;/h1&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ROM_DB = if RUBY_ENGINE == "jruby"
  require "jdbc/sqlite3"
  require "sequel"
  Sequel.connect("jdbc:sqlite::memory:rom")
else
  require "sqlite3"
  require "sequel"
  Sequel.connect("sqlite::memory:rom")
end

require "rom"
require "rom-sql"

# Define the ROM relations
class RomTestIdentities &amp;lt; ROM::Relation[:sql]
  schema(:rom_test_identities) do
    attribute :id, ROM::SQL::Types::Serial
    attribute :email, ROM::SQL::Types::String
    attribute :login, ROM::SQL::Types::String
    attribute :password_digest, ROM::SQL::Types::String
    attribute :pwd_hash, ROM::SQL::Types::String
    attribute :owner_id, ROM::SQL::Types::Integer
  end
end

class RomTestOwners &amp;lt; ROM::Relation[:sql]
  schema(:rom_test_owners) do
    attribute :id, ROM::SQL::Types::Serial
    attribute :name, ROM::SQL::Types::String
  end
end

# Set up ROM container
ROM_CONFIG = ROM::Configuration.new(:sql, ROM_DB)
ROM_CONFIG.register_relation(RomTestIdentities)
ROM_CONFIG.register_relation(RomTestOwners)
ROM_CONTAINER = ROM.container(ROM_CONFIG)

class RomTestIdentity
  include OmniAuth::Identity::Models::Rom

  # Configure via the new DSL (no `self.`): accepts a value or a proc
  rom_container -&amp;gt; { ROM_CONTAINER }
  rom_relation_name :rom_test_identities
  # Use the standard Model.auth_key API to configure the auth key
  auth_key :email
  password_field :password_digest

  # Provide a simple reader for the :login attribute for specs that use a
  # non-standard auth key (e.g. `auth_key :login`). The ROM adapter stores the
  # underlying tuple in @identity_data, so delegate to it here.
  def login
    @identity_data &amp;amp;&amp;amp; @identity_data[:login]
  end
end
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h1&gt;
  
  
  Test
&lt;/h1&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;RSpec.describe(OmniAuth::Identity::Models::Rom, :sqlite3) do
  before(:all) do
    # Create the tables
    ROM_DB.create_table(:rom_test_identities) do
      primary_key :id
      String :email, null: false
      String :password_digest, null: false
      # Add the login column to support tests that use a non-standard auth_key
      String :login
      Integer :owner_id
    end

    ROM_DB.create_table(:rom_test_owners) do
      primary_key :id
      String :name, null: false
    end
  end

  before do
    # Clear the tables before each test
    ROM_DB[:rom_test_identities].delete
    ROM_DB[:rom_test_owners].delete
  end

  describe "model", type: :model do
    subject(:model_klass) { RomTestIdentity }

    describe "::authenticate" do
      it "authenticates with correct password" do
        # Insert test data
        password_digest = BCrypt::Password.create("password")
        identity_data = {email: "test@example.com", password_digest: password_digest}
        ROM_CONTAINER.relations[:rom_test_identities].insert(identity_data)

        authenticated = model_klass.authenticate({email: "test@example.com"}, "password")
        expect(authenticated).to(be_a(RomTestIdentity))
        expect(authenticated.email).to(eq("test@example.com"))
      end

      it "authenticates with custom auth_key (login) when auth_key is set to :login" do
        original = model_klass.auth_key
        model_klass.auth_key(:login)

        begin
          # Insert test data using login field
          password_digest = BCrypt::Password.create("password")
          identity_data = {login: "bob", email: "bob@example.com", password_digest: password_digest}
          ROM_CONTAINER.relations[:rom_test_identities].insert(identity_data)

          authenticated = model_klass.authenticate({login: "bob"}, "password")
          expect(authenticated).to(be_a(RomTestIdentity))
          expect(authenticated.login).to(eq("bob"))
        ensure
          model_klass.auth_key(original)
        end
      end

      it "returns false with incorrect password" do
        # Insert test data
        password_digest = BCrypt::Password.create("password")
        identity_data = {email: "test@example.com", password_digest: password_digest}
        ROM_CONTAINER.relations[:rom_test_identities].insert(identity_data)

        authenticated = model_klass.authenticate({email: "test@example.com"}, "wrong")
        expect(authenticated).to(be(false))
      end

      it "returns false for non-existent user" do
        authenticated = model_klass.authenticate({email: "nonexistent@example.com"}, "password")
        expect(authenticated).to(be(false))
      end
    end
  end
end
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Photo by &lt;a href="https://unsplash.com/@ryunosuke_kikuno?utm_source=unsplash&amp;amp;utm_medium=referral&amp;amp;utm_content=creditCopyText" rel="noopener noreferrer"&gt;Ryunosuke Kikuno&lt;/a&gt; on &lt;a href="https://unsplash.com/photos/a-white-swan-with-a-yellow-beak-dips-its-head-911slHGPFpY?utm_source=unsplash&amp;amp;utm_medium=referral&amp;amp;utm_content=creditCopyText" rel="noopener noreferrer"&gt;Unsplash&lt;/a&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>ruby</category>
      <category>rom</category>
      <category>authentication</category>
    </item>
    <item>
      <title>💎 ANN: oauth2 v2.0.17</title>
      <dc:creator>Peter H. Boling</dc:creator>
      <pubDate>Thu, 18 Sep 2025 06:30:38 +0000</pubDate>
      <link>https://dev.to/galtzo/ann-oauth2-v2017-h1a</link>
      <guid>https://dev.to/galtzo/ann-oauth2-v2017-h1a</guid>
      <description>&lt;h1&gt;
  
  
  Announcing oauth2 v2.0.17 — Static Hash for verb‑dependent token mode (Instagram‑friendly)
&lt;/h1&gt;

&lt;p&gt;The oauth2 v2.0.17 is a small but useful release: it adds support for configuring verb‑dependent token transmission with a static Hash in addition to the previously available Proc. This makes integrations like Instagram’s Graph API a little simpler and slightly more performant.&lt;/p&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;v2.0.15 introduced verb‑dependent token mode, so you could decide per HTTP verb whether the access token should be sent in the query string or the Authorization header.&lt;/li&gt;
&lt;li&gt;v2.0.17 lets you provide that mapping as a static Hash instead of a Proc.&lt;/li&gt;
&lt;li&gt;Example mapping: &lt;code&gt;{get: :query, post: :header, delete: :header}&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why this matters
&lt;/h2&gt;

&lt;p&gt;Some APIs (notably Instagram’s Graph API) require you to send the access token in the query for GET requests (e.g., &lt;code&gt;?access_token=...&lt;/code&gt;), but in the Authorization header for POST/DELETE (e.g., &lt;code&gt;Authorization: Bearer ...&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;In v2.0.15 we added support for a verb‑dependent mode via a Proc, like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;mode: &lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;verb&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;verb&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="ss"&gt;:get&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="ss"&gt;:query&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="ss"&gt;:header&lt;/span&gt; &lt;span class="p"&gt;}}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In v2.0.17 you can now configure the same behavior using a static Hash, which avoids calling a Proc for each request, keeps intent obvious at a glance, and can be trivially serialized or reused:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;verb_dependent_token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;get: :query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;post: :header&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;delete: :header&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Example: Instagram Graph API with a static Hash
&lt;/h2&gt;

&lt;p&gt;Below is a concrete example that exchanges a short‑lived token for a long‑lived token, refreshes it, and then makes API calls — all while automatically placing the token in the right place per HTTP verb using the static Hash mode.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="nb"&gt;require&lt;/span&gt; &lt;span class="s2"&gt;"oauth2"&lt;/span&gt;

&lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;OAuth2&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kp"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kp"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;site: &lt;/span&gt;&lt;span class="s2"&gt;"https://graph.instagram.com"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Start with a short‑lived token you already obtained via Facebook Login&lt;/span&gt;
&lt;span class="n"&gt;verb_dependent_token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;get: :query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;post: :header&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;delete: :header&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;short_lived&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;OAuth2&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;AccessToken&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="no"&gt;ENV&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"IG_SHORT_LIVED_TOKEN"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="ss"&gt;mode: &lt;/span&gt;&lt;span class="n"&gt;verb_dependent_token&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# 1) Exchange for a long‑lived token (GET with token in query)&lt;/span&gt;
&lt;span class="c1"&gt;#    Endpoint: GET https://graph.instagram.com/access_token&lt;/span&gt;
&lt;span class="c1"&gt;#    Params: grant_type=ig_exchange_token, client_secret=APP_SECRET&lt;/span&gt;
&lt;span class="n"&gt;exchange&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;short_lived&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="s2"&gt;"/access_token"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="ss"&gt;params: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="ss"&gt;grant_type: &lt;/span&gt;&lt;span class="s2"&gt;"ig_exchange_token"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="ss"&gt;client_secret: &lt;/span&gt;&lt;span class="no"&gt;ENV&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"IG_APP_SECRET"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="c1"&gt;# access_token will be added automatically in the query&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;long_lived_token_value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;exchange&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parsed&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"access_token"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="n"&gt;long_lived&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;OAuth2&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;AccessToken&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;long_lived_token_value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="ss"&gt;mode: &lt;/span&gt;&lt;span class="n"&gt;verb_dependent_token&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# 2) Refresh the long‑lived token (GET with token in query)&lt;/span&gt;
&lt;span class="c1"&gt;#    Endpoint: GET https://graph.instagram.com/refresh_access_token&lt;/span&gt;
&lt;span class="n"&gt;refresh_resp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;long_lived&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="s2"&gt;"/refresh_access_token"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="ss"&gt;params: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;grant_type: &lt;/span&gt;&lt;span class="s2"&gt;"ig_refresh_token"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;long_lived&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;OAuth2&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;AccessToken&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;refresh_resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parsed&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"access_token"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="ss"&gt;mode: &lt;/span&gt;&lt;span class="n"&gt;verb_dependent_token&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# 3) Typical API GET request (token automatically in query)&lt;/span&gt;
&lt;span class="n"&gt;me&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;long_lived&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"/me"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;params: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;fields: &lt;/span&gt;&lt;span class="s2"&gt;"id,username"&lt;/span&gt;&lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nf"&gt;parsed&lt;/span&gt;

&lt;span class="c1"&gt;# 4) Example POST (token automatically in Authorization header)&lt;/span&gt;
&lt;span class="c1"&gt;# long_lived.post("/me/media", body: {image_url: "https://...", caption: "hello"})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notes&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Instagram is a special case that explicitly requires query‑string tokens for GET endpoints. For most providers you should prefer header‑based tokens when possible.&lt;/li&gt;
&lt;li&gt;If you need custom logic beyond a simple mapping, you can still use a Proc: &lt;code&gt;mode: -&amp;gt;(verb) { ... }&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Migrating from Proc to Hash
&lt;/h2&gt;

&lt;p&gt;If you already use the v2.0.15 Proc style:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;mode: &lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;verb&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;verb&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="ss"&gt;:get&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="ss"&gt;:query&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="ss"&gt;:header&lt;/span&gt; &lt;span class="p"&gt;}}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can switch to the Hash form in v2.0.17:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;mode: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;get: :query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;post: :header&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;delete: :header&lt;/span&gt;&lt;span class="p"&gt;}}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both are supported; choose whichever best fits your app. The Hash form is generally a bit faster and more explicit, while the Proc form is endlessly versatile!&lt;/p&gt;

&lt;h2&gt;
  
  
  Release links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Changelog entry: &lt;a href="https://github.com/ruby-oauth/oauth2/blob/main/CHANGELOG.md#2017---2025-09-15" rel="noopener noreferrer"&gt;https://github.com/ruby-oauth/oauth2/blob/main/CHANGELOG.md#2017---2025-09-15&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Tag: &lt;a href="https://github.com/ruby-oauth/oauth2/releases/tag/v2.0.17" rel="noopener noreferrer"&gt;https://github.com/ruby-oauth/oauth2/releases/tag/v2.0.17&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;PR: Add Hash‑based verb‑dependent mode &lt;a href="https://github.com/ruby-oauth/oauth2/pull/682" rel="noopener noreferrer"&gt;https://github.com/ruby-oauth/oauth2/pull/682&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Thanks
&lt;/h2&gt;

&lt;p&gt;Thanks to everyone using oauth2 and filing issues. Keep the feedback coming!&lt;/p&gt;

&lt;h2&gt;
  
  
  Support &amp;amp; Funding Info
&lt;/h2&gt;

&lt;p&gt;I am a full-time FLOSS maintainer. If you find &lt;a href="//github.com/pboling"&gt;my work&lt;/a&gt; valuable I ask that you become a sponsor. Every dollar helps!&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;🥰 Support FLOSS work 🥰&lt;/th&gt;
&lt;th&gt;Get access&lt;/th&gt;
&lt;th&gt;"Sponsors" channel&lt;/th&gt;
&lt;th&gt;on Galtzo FLOSS&lt;/th&gt;
&lt;th&gt;Discord 👇️ &lt;a href="https://discord.gg/3qme4XHNKN" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraster.shields.io%2Fdiscord%2F1373797679469170758%3Fstyle%3Dfor-the-badge" alt="Live Chat on Discord" width="144" height="28"&gt;&lt;/a&gt;
&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;a href="https://opencollective.com/ruby-oauth#backer" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraster.shields.io%2Fopencollective%2Fbackers%2Fruby-oauth.png%3Fstyle%3Dfor-the-badge" alt="OpenCollective Backers" width="112" height="28"&gt;&lt;/a&gt; &lt;a href="https://opencollective.com/ruby-oauth#sponsor" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraster.shields.io%2Fopencollective%2Fsponsors%2Fruby-oauth.png%3Fstyle%3Dfor-the-badge" alt="OpenCollective Sponsors" width="122" height="28"&gt;&lt;/a&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;a href="https://www.buymeacoffee.com/pboling" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraster.shields.io%2Fbadge%2Fbuy_me_a_coffee-%25E2%259C%2593-a51611.png%3Fstyle%3Dflat" alt="Buy me a coffee" width="118" height="20"&gt;&lt;/a&gt; &lt;a href="https://ko-fi.com/O5O86SNP4" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraster.shields.io%2Fbadge%2Fko--fi-%25E2%259C%2593-a51611.png%3Fstyle%3Dflat" alt="Donate at ko-fi.com" width="54" height="20"&gt;&lt;/a&gt;
&lt;/td&gt;
&lt;td&gt;&lt;a href="https://www.paypal.com/paypalme/peterboling" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraster.shields.io%2Fbadge%2Fdonate-paypal-a51611.png%3Fstyle%3Dflat%26logo%3Dpaypal" alt="Donate on PayPal" width="111" height="20"&gt;&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href="https://polar.sh/pboling" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraster.shields.io%2Fbadge%2Fpolar-donate-a51611.png%3Fstyle%3Dflat" alt="Donate on Polar" width="84" height="20"&gt;&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;
&lt;a href="https://github.com/sponsors/pboling" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraster.shields.io%2Fbadge%2FSponsor_Me%21-pboling.png%3Fstyle%3Dsocial%26logo%3Dgithub" alt="Sponsor Me on Github" width="107" height="20"&gt;&lt;/a&gt; &lt;a href="https://liberapay.com/pboling/donate" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraster.shields.io%2Fliberapay%2Fgoal%2Fpboling.png%3Flogo%3Dliberapay%26color%3Da51611%26style%3Dflat" alt="Liberapay Goal Progress" width="131" height="20"&gt;&lt;/a&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Photo (cropped) by &lt;a href="https://unsplash.com/@wonderkim?utm_content=creditCopyText&amp;amp;utm_medium=referral&amp;amp;utm_source=unsplash" rel="noopener noreferrer"&gt;Wonder KIM&lt;/a&gt; on &lt;a href="https://unsplash.com/photos/a-dog-wearing-a-sweater-and-boots-in-the-snow-puTqUxcMZz8?utm_content=creditCopyText&amp;amp;utm_medium=referral&amp;amp;utm_source=unsplash" rel="noopener noreferrer"&gt;Unsplash&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ruby</category>
      <category>oauth</category>
      <category>webdev</category>
      <category>instagram</category>
    </item>
    <item>
      <title>💎 ANN: kettle-dev v1.1.20 w/ improved CHANGELOG handling</title>
      <dc:creator>Peter H. Boling</dc:creator>
      <pubDate>Mon, 15 Sep 2025 11:53:14 +0000</pubDate>
      <link>https://dev.to/galtzo/ann-kettle-dev-v1020-w-improved-changelog-handling-20hl</link>
      <guid>https://dev.to/galtzo/ann-kettle-dev-v1020-w-improved-changelog-handling-20hl</guid>
      <description>&lt;h2&gt;
  
  
  &lt;a href="https://github.com/kettle-rb/kettle-dev/compare/v1.1.19...v1.1.20" rel="noopener noreferrer"&gt;1.1.20&lt;/a&gt; - 2025-09-15
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;TAG: &lt;a href="https://github.com/kettle-rb/kettle-dev/releases/tag/v1.1.20" rel="noopener noreferrer"&gt;v1.1.20&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;COVERAGE: 96.80% -- 3660/3781 lines in 25 files&lt;/li&gt;
&lt;li&gt;BRANCH COVERAGE: 81.65% -- 1504/1842 branches in 25 files&lt;/li&gt;
&lt;li&gt;77.01% documented&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Added
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Allow reformating of CHANGELOG.md without version bump&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;--include=GLOB&lt;/code&gt; includes files not otherwise included in default template&lt;/li&gt;
&lt;li&gt;more test coverage&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Fixed
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Add .licenserc.yaml to gem package&lt;/li&gt;
&lt;li&gt;Handling of GFM fenced code blocks in CHANGELOG.md&lt;/li&gt;
&lt;li&gt;Handling of nested list items in CHANGELOG.md&lt;/li&gt;
&lt;li&gt;Handling of blank lines around all headings in CHANGELOG.md&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Support &amp;amp; Funding Info
&lt;/h2&gt;

&lt;p&gt;I am a full-time FLOSS maintainer. If you find &lt;a href="//github.com/pboling"&gt;my work&lt;/a&gt; valuable I ask that you become a sponsor. Every dollar helps!&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;🥰 Support FLOSS work 🥰&lt;/th&gt;
&lt;th&gt;Get access&lt;/th&gt;
&lt;th&gt;"Sponsors" channel&lt;/th&gt;
&lt;th&gt;on Galtzo FLOSS&lt;/th&gt;
&lt;th&gt;Discord 👇️ &lt;a href="https://discord.gg/3qme4XHNKN" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraster.shields.io%2Fdiscord%2F1373797679469170758%3Fstyle%3Dfor-the-badge" alt="Live Chat on Discord" width="144" height="28"&gt;&lt;/a&gt;
&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;a href="https://opencollective.com/ruby-oauth#backer" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraster.shields.io%2Fopencollective%2Fbackers%2Fruby-oauth.png%3Fstyle%3Dfor-the-badge" alt="OpenCollective Backers" width="112" height="28"&gt;&lt;/a&gt; &lt;a href="https://opencollective.com/ruby-oauth#sponsor" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraster.shields.io%2Fopencollective%2Fsponsors%2Fruby-oauth.png%3Fstyle%3Dfor-the-badge" alt="OpenCollective Sponsors" width="122" height="28"&gt;&lt;/a&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;a href="https://www.buymeacoffee.com/pboling" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraster.shields.io%2Fbadge%2Fbuy_me_a_coffee-%25E2%259C%2593-a51611.png%3Fstyle%3Dflat" alt="Buy me a coffee" width="118" height="20"&gt;&lt;/a&gt; &lt;a href="https://ko-fi.com/O5O86SNP4" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraster.shields.io%2Fbadge%2Fko--fi-%25E2%259C%2593-a51611.png%3Fstyle%3Dflat" alt="Donate at ko-fi.com" width="54" height="20"&gt;&lt;/a&gt;
&lt;/td&gt;
&lt;td&gt;&lt;a href="https://www.paypal.com/paypalme/peterboling" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraster.shields.io%2Fbadge%2Fdonate-paypal-a51611.png%3Fstyle%3Dflat%26logo%3Dpaypal" alt="Donate on PayPal" width="111" height="20"&gt;&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href="https://polar.sh/pboling" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraster.shields.io%2Fbadge%2Fpolar-donate-a51611.png%3Fstyle%3Dflat" alt="Donate on Polar" width="84" height="20"&gt;&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;
&lt;a href="https://github.com/sponsors/pboling" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraster.shields.io%2Fbadge%2FSponsor_Me%21-pboling.png%3Fstyle%3Dsocial%26logo%3Dgithub" alt="Sponsor Me on Github" width="107" height="20"&gt;&lt;/a&gt; &lt;a href="https://liberapay.com/pboling/donate" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraster.shields.io%2Fliberapay%2Fgoal%2Fpboling.png%3Flogo%3Dliberapay%26color%3Da51611%26style%3Dflat" alt="Liberapay Goal Progress" width="131" height="20"&gt;&lt;/a&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

</description>
      <category>devtools</category>
      <category>ruby</category>
      <category>packaging</category>
      <category>automation</category>
    </item>
  </channel>
</rss>
