<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom"><generator uri="https://gohugo.io/" version="0.147.5">Hugo</generator><title>André Arko</title><link href="http://andre.arko.net/atom.xml" rel="self"/><link href="http://andre.arko.net/"/><updated>2026-08-10T04:38:03+00:00</updated><id>http://andre.arko.net/</id><author><name>André Arko</name><email>andre@arko.net</email></author><entry><title type="html">Ruby Central's Destructive Legacy</title><link href="https://andre.arko.net/2026/07/30/ruby-centrals-destructive-legacy/"/><updated>2026-07-30T00:00:00+00:00</updated><id>http://andre.arko.net/2026/07/30/ruby-centrals-destructive-legacy/</id><content type="html"><![CDATA[<p>At conferences and meetups, and in conversations online, many Ruby developers have asked me about Ruby Central and my disagreement with them. This post is my attempt to answer the questions I&rsquo;ve been getting over and over, covering: 1) what is Ruby Central doing now? 2) has the dispute over Bundler and RubyGems been resolved? 3) what has Ruby Central said about the dispute? and 4) what can Ruby developers do now?</p>
<h3 id="what-is-ruby-central-doing-now">What is Ruby Central doing now?</h3>
<p>To talk about what Ruby Central is doing now, we need to start with a bit of historical context. What was Ruby Central doing before this saga began? About 18 months ago, Ruby Central:</p>
<ul>
<li>ran 2 conferences every year, RubyConf and RailsConf</li>
<li>had an <a href="https://rubycentral.org/news/a-new-chapter-for-rubygems-how-ruby-central-is-building-a-sustainable-future/#the-ruby-central-oss-committee-is-formed">Open Source Committee</a> with 3 members (Ufuk, Gabi, Mike)</li>
<li>funded 7 <a href="https://web.archive.org/web/20250602004248/https://rubycentral.org/open-source/#team-heading">part-time community OSS contributors</a> (André, Arun, Ellen, Gift, Irene, Josef, Martin)</li>
<li>funded 3 <a href="https://web.archive.org/web/20250602004248/https://rubycentral.org/open-source/#team-heading">full-time OSS developers</a> (Colby, Samuel, David)</li>
<li>coordinated those 10 OSS contributors across 3 major OSS projects:
<ul>
<li>RubyGems</li>
<li>Bundler</li>
<li>RubyGems.org</li>
</ul>
</li>
</ul>
<p>Then, about 10 months ago, Ruby Central began what core team member Ellen Dash called <a href="https://mjtsai.com/blog/2025/09/23/ruby-central-takes-over-rubygems/">a &ldquo;hostile takeover&rdquo; of the RubyGems, Bundler, and RubyGems.org open source projects</a>, seizing control of the projects and locking out the team that had nurtured and maintained them for over a decade.</p>
<p>Today, 10 months after their takeover, Ruby Central has:</p>
<ul>
<li>lost 2 of 2 conferences, after <a href="https://rubycentral.org/news/announcing-railsconf-2025-and-a-new-chapter-for-ruby-central-events/">ending RailsConf</a>, <a href="https://rubycentral.org/news/rubyconf-updates/">cancelling a significant portion of the last RubyConf</a>, and providing no future conference locations or dates for the first time in decades</li>
<li>lost 2 of 3 major OSS projects, by <a href="https://www.ruby-lang.org/en/news/2025/10/17/rubygems-repository-transition/">transferring Bundler &amp; RubyGems away to Matz</a></li>
<li>lost 9 of 10 <a href="https://web.archive.org/web/20250602004248/https://rubycentral.org/open-source/#team-heading">contributors to open source work</a>, including:
<ul>
<li>6 of 7 operators of RubyGems.org (André, Samuel, Arun, Josef, Ellen, Martin)</li>
<li>2 of 3 full-time contributors to OSS (Samuel, David)</li>
<li>2 of 2 OSS writers (Gift, Irene)</li>
</ul>
</li>
<li>lost 3 of 3 members of the <a href="https://rubycentral.org/about/">Open Source Committee</a> (Gabi, Mike, Ufuk)</li>
<li>lost 5 of 7 <a href="https://web.archive.org/web/20250902024234/https://rubycentral.org/about/">board members</a> (Kinsey, Valerie, Naijeria, Ben, Ufuk)</li>
<li>lost 3 of 3 Executive Directors (Adarsh, Chelsea, Shan)</li>
<li>lost 2 of 2 major financial sponsors (Sidekiq and Shopify, although <a href="https://rubycentral.org/news/shopify-joins-the-ruby-alliance/">Shopify seemingly just returned</a> after a 9 month break)</li>
</ul>
<p>Unlike many community non-profits (including the <a href="https://www.python.org/psf/bylaws/">Python Software Foundation</a>), Ruby Central <a href="https://rubycentral.org/news/jointheboard/">does not hold elections</a> for their board of directors. Despite <a href="https://rubycentral.org/news/modernizing-ruby-centrals-bylaws-and-officer-updates/">rewriting their bylaws entirely this year</a>, new directors are still selected exclusively by existing directors, with no public process for input or feedback. The current board consists of just 2 of the 7 members who initially approved the hostile takeover, plus 3 new members chosen by the previous board (Brandon, Jey, and Ran).</p>
<p>Based on Ruby Central&rsquo;s <a href="https://rubycentral.org/news/">public announcements</a>, they have added exactly one new program after ending both conferences and transferring away half of their open source software: <a href="https://web.archive.org/web/20260616121600/https://rubycentral.org/news/strengthening-security-for-the-ruby-ecosystem/">a new security project</a>, funded by a grant from <a href="https://alpha-omega.dev/">Alpha-Omega</a>. The security project&rsquo;s goal is to use Anthropic&rsquo;s Project Glasswing to search for security issues in gems that have already been published. In their <a href="https://github.com/ossf/alpha-omega/blob/main/alpha/engagements/2026/Ruby%20Central/2026-06.md">first monthly report for June</a>, they reported finding a total of 5 vulnerabilities, with 1 issue at medium severity, and 4 at low or unclassified severities.</p>
<p>Ruby Central&rsquo;s current sponsorship drive, the Ruby Alliance, has found three members willing to join: Gusto, Thoughtbot, and (very recently) Shopify. What none of the <a href="https://rubycentral.org/news/gusto-joins-the-ruby-alliance/">sponsorship</a> <a href="https://rubycentral.org/news/thoughtbot-joins-the-ruby-alliance/">announcement</a> <a href="https://thoughtbot.com/blog/why-thoughtbot-is-joining-the-ruby-alliance">posts</a> have mentioned is that Gusto and Thoughtbot employ two of the five <a href="https://rubycentral.org/about/">Ruby Central board members</a>. <a href="https://www.linkedin.com/in/davidcorsonknowles/">David Corson-Knowles</a> is both a board member and a Senior Staff Engineer at Gusto. <a href="https://www.linkedin.com/in/craycraft/">Ran Craycraft</a> is not just President of the Ruby Central board, he is also Managing Director, Americas at Thoughtbot. That means the board members who allocate the sponsorship money are also providing that sponsorship money, a conflict of interest which neither the companies nor Ruby Central have disclosed. The final sponsor is Shopify, <a href="https://rubycentral.org/news/shopify-joins-the-ruby-alliance/">coming back 9 months after</a> <a href="https://joel.drapper.me/p/rubygems-takeover/">it was reported they demanded Ruby Central start this disaster in the first place</a>.</p>
<h3 id="has-our-dispute-been-resolved">Has our dispute been resolved?</h3>
<p>No, our dispute has not been resolved. Since last October, Ruby Central has been threatening to sue me. They have not withdrawn that threat as of today.</p>
<p>The core of our dispute is that last September, Ruby Central <a href="https://joel.drapper.me/p/rubygems-takeover/">hijacked the Bundler and RubyGems GitHub projects from their maintainers of over ten years</a>. When I informed Ruby Central that <a href="https://andre.arko.net/2025/09/25/bundler-belongs-to-the-ruby-community/">I own the name Bundler</a>, they retaliated with a threat to sue me for <a href="https://rubycentral.org/news/rubygems-org-aws-root-access-event-september-2025/">supposed &ldquo;hacking&rdquo;</a>, a position <a href="https://andre.arko.net/2025/10/09/the-rubygems-security-incident/">I have always disputed</a>.</p>
<p>Ruby Central has stated they will only withdraw their threat if I drop my claim they infringed the name Bundler. After I also questioned if my work as a contractor had been allowed by state labor law, they additionally started demanding that I drop any claims regarding employment as well. So far, Ruby Central has consistently said they will only withdraw their lawsuit threat if I give up my infringement claim for Bundler and any possible back pay I believe they owe me.</p>
<p>When the lawsuit threat did not make me drop my claims, <a href="https://www.ruby-lang.org/en/news/2025/10/17/rubygems-repository-transition/">Ruby Central then chose to give away the Bundler gem and <code>rubygems</code> GitHub repo to Matz</a>. After that, all of the former maintainers made <a href="https://andre.arko.net/2025/10/26/we-want-to-move-ruby-forward/">a new offer to settle</a>. Ruby Central never replied to our offer. Instead, they restated their original offer, and only to me: they would withdraw their threat to sue only if I dropped all of my claims. I did not accept, and after four months with no further progress, I wrote <a href="https://andre.arko.net/2026/03/03/four-months-of-ruby-central-moving-ruby-backward/">a public update</a>.</p>
<p>In response to my post, Ruby Central did two things at once:</p>
<p>First, Ruby Central told me they were actively seeking outside funding to pay a settlement to me that would end our dispute. They asked me for an amount that would fully compensate me for my attorney&rsquo;s fees, the amount they might owe in back pay, plus compensation for an additional six months after I was fired. With help from an expert, I calculated an amount around $450,000, and provided that number as requested.</p>
<p>Despite providing that number, I have never asked for that amount of money. I provided that number at Ruby Central&rsquo;s request, so they could seek funding. What I have instead asked for, consistently, is an apology for the attacks on my reputation without evidence, and the far smaller amount of reimbursement for my attorneys&rsquo; fees.</p>
<p>Second, at the same time as they sought funding for a settlement, Ruby Central also reported me to the FBI through their lawyer, requesting that I be criminally investigated. On March 9, 2026, Ruby Central&rsquo;s lawyer informed my lawyer that he had made the report, and suggested I would need to hire a criminal defense attorney. As far as I have been able to learn, Ruby Central&rsquo;s lawyer was not able to provide any evidence to the FBI with his report, since Ruby Central has not been able to find evidence that I caused any harm during their three audits of the RubyGems AWS account.</p>
<p>Unfortunately, it is impossible to withdraw a report to the FBI. Despite Ruby Central&rsquo;s later statement to me that they don&rsquo;t intend to pursue the FBI report, they gave up the ability to make that decision when they filed the report — it&rsquo;s up to the FBI now, not up to them.</p>
<p>A few weeks after saddling me with a permanent possibility of criminal prosecution by the FBI, Ruby Central <a href="https://rubycentral.org/news/a-new-chapter-for-ruby-central/">ran out of money</a>. Their post said they &ldquo;want to move forward together&rdquo;, but did not take any action or answer any questions regarding their past actions. Nonetheless taking that post as a promising sign, I offered Ruby Central <a href="https://andre.arko.net/2026/04/02/towards-an-amicable-resolution-with-ruby-central/">a new option to resolve our disagreement</a>.</p>
<p>I did not ask them to return Bundler. I did not ask them for fair payment under employment law. I didn&rsquo;t even ask for an apology. Instead, I asked for 1) public confirmation that I did not cause any harm to RubyGems, and 2) reimbursement for the lawyer fees I have had to pay because of their threatened lawsuit. Ruby Central then ignored that post, and multiple attempts to reach out, for six weeks.</p>
<p>After six weeks, Ruby Central sent me a rerun of their settlement offer, with some specific restrictions: they would withdraw their lawsuit threat if I drop my claim they infringed the Bundler trademark, drop my claim they violated employment law, and agree to non-disparagement that binds me but does not bind Ruby Central, instead only specific individuals currently associated with them.</p>
<p>This offer did contain one new twist. Ruby Central wrote that if I accepted they would limit their cooperation with the FBI, offering to &ldquo;discourage the filing of criminal charges, and if an investigation is opened or charges filed, cooperate only to the extent required by law&rdquo;. There&rsquo;s just one small problem with that offer: many state and federal laws make it a crime to agree to any limits on cooperation with a criminal investigation. Needless to say, I could not accept such a settlement.</p>
<p>Since Ruby Central can&rsquo;t withdraw their FBI report (because it&rsquo;s out of their hands now), and they can&rsquo;t give back the repos they took (<a href="https://www.ruby-lang.org/en/news/2025/10/17/rubygems-repository-transition">because they don&rsquo;t have them anymore</a>), and they can&rsquo;t pay any kind of compensation (because they don&rsquo;t have any money), I was at a bit of a loss. What could Ruby Central even do to try to resolve this situation that they had created?</p>
<p>In the end, I responded with a counter-offer of my own: I would drop my infringement claim against Ruby Central over their use of the name Bundler, and I would drop my claim Ruby Central violated employment law, if they would withdraw their threat to sue me and publish an apology. I asked them to apologize for publicly accusing me of harming RubyGems.org without evidence, apologize for claiming I tried to obtain user PII when I did not, and apologize for breaking the Bundler and RubyGems governance to execute their takeover. Surprising me, Ruby Central did not object to the apology I requested.</p>
<p>Unfortunately, Ruby Central has continued to insist on a non-disparagement agreement that binds me, but does not bind Ruby Central. I was forced to reply that I could not accept such an unbalanced agreement.</p>
<p>It has been 30 days since my counter-offer. While RubyConf did take up 3 of those days, Ruby Central has not replied for the full 30 days. Their lawyer promised to provide a timeframe for their response within one day, and has not done even that. A month with no response points to a different kind of resolution: an incredibly disappointing legacy for the shattered remains of a once-legendary organization.</p>
<h2 id="what-has-ruby-central-said-about-this">What has Ruby Central said about this?</h2>
<p>Even after all of the consequences described here, Ruby Central has never acknowledged that <a href="https://mensfeld.pl/2025/09/ruby-central-rubygems-takeover-analysis/">maintainers of over ten years should not have been thrown out of their own projects</a>. Ruby Central either does not know, or is not willing to admit, that they needlessly attacked and removed the existing team when we could have given them all the help they needed to secure their systems. Ruby Central&rsquo;s public posts to date <a href="https://rubycentral.org/news/a-new-chapter-for-ruby-central/#to-the-maintainers">carefully offer no apologies</a> to the maintainers.</p>
<p>The sole public justification they have ever offered for this hostile takeover is that they did this &ldquo;<a href="https://rubycentral.org/news/strengthening-the-stewardship-of-rubygems-and-bundler/">to improve security</a>&rdquo;. Unfortunately, Ruby Central removed all of the experts who could have ensured they executed their takeover securely. When I responsibly disclosed Ruby Central&rsquo;s failure to secure their systems, which embarrassed them, they retaliated by <a href="https://rubycentral.org/news/rubygems-org-aws-root-access-event-september-2025/">publicly accusing me of &ldquo;hacking&rdquo;</a> and <a href="https://andre.arko.net/2025/10/09/the-rubygems-security-incident/">privately preparing to sue me</a>.</p>
<p>As of this writing, Ruby Central has conducted no less than <a href="https://github.com/ossf/alpha-omega/blob/main/alpha/engagements/2025/Ruby%20Central/update-2025-10.md#unauthorized-access-and-credentials-rotation">three</a> <a href="https://github.com/ossf/alpha-omega/blob/main/alpha/engagements/2026/Ruby%20Central/2026-01.md#aws-security-audit">separate</a> <a href="https://rubycentral.org/news/a-message-from-the-ruby-central-board/">audits</a>, seeking evidence that I caused some sort of harm. They have not found any evidence, because (beyond <a href="https://joel.drapper.me/p/ruby-central-security-measures/">revealing Ruby Central&rsquo;s embarrassing failure to secure the RubyGems.org service</a>) I did not take, damage, or harm any part the RubyGems.org service.</p>
<p>During the multiple months where Ruby Central ignored my offers to settle, after baselessly reporting me to the FBI, they nevertheless published a &ldquo;<a href="https://rubycentral.org/news/rubygems-fracture-incident-report/">Fracture Incident Report</a>&rdquo;, which states it was written to &ldquo;provide closure&rdquo;. Unfortunately for that closure, the report does not explain any of the reasons Ruby Central chose a path that destroyed their sponsorships, their volunteers, their open source projects, their staff, and their board of directors.</p>
<p>For example, what actions did the board actually vote to approve? What did Ruby Central expect to gain? Why did Ruby Central single me out personally, while ignoring the other maintainers? Why jump straight from being embarrassed to threatening a lawsuit? What could possibly justify involving the FBI while there is no evidence of any harm? The report answers none of these questions.</p>
<p>Instead, Ruby Central continues former Executive Director Shan Cureton&rsquo;s public policy, even beyond her departure: no one deserves to know why any of this happened, because &ldquo;<a href="https://youtu.be/nKpo68g9dEk?si=317CcZWMDIJNYZQ3&amp;t=949">they don&rsquo;t need to have the story, and it isn&rsquo;t their story to have</a>&rdquo;. According to Ruby Central, I don&rsquo;t deserve to know why this happened, none of the former maintainers deserve to know why this happened, and no one else in the Ruby community deserves to know why this happened. Including you.</p>
<h3 id="what-can-we-do-about-this">What can we do about this?</h3>
<p>There was a time when Ruby Central did more work to support the Ruby community than any other organization in the United States. In 2004, when I first started learning Ruby, I was surprised to discover that RubyConf 2005 would be in San Diego, close enough for me to attend. I rounded up my only two friends who also liked programming, and we went together.</p>
<p>I saw Matz give a talk proposing the <code>-&gt;</code> &ldquo;stabby lambda&rdquo; syntax. I tried to understand <code>call_cc</code> in a talk given by Jim Weirich using a Nintendo 64 to demonstrate continuations via <em>The Legend of Zelda: Ocarina of Time</em>. I saw the Rails core team, working toward releasing Rails 1.0 in December of that year. I joined IRC on Freenode, where I made a huge number of Ruby friends I still have today, over 20 years later. I met other students who were attending from nearby UCSD, and we are also still friends to this day.</p>
<p>Just over a year later, I attended the first RailsConf in 2006. I met some strangers, we became friends, and one of them invited me to apply to work at his company. That became my first full-time job, set an unrealistically high standard for managers and co-workers, and made me even more friends that I still talk to regularly today.</p>
<p>Ruby Central&rsquo;s conferences changed my life, in the best way possible. The community that grew out of the events, talks, online chats, and shared code inspired me to contribute back — to help create and ship Bundler 1.0 in 2009, to maintain Bundler for the next 15 years, to work on RubyGems and RubyGems.org for over 10 years, and to found the non-profit Ruby Together to focus on funding and maintenance for 7 years.</p>
<p>When Ruby Central reached out to express interest in merging with Ruby Together, and combining my open source programs with their conference programs, I was flattered. I agreed to the merger based on their spectacular track record of conferences and community-building from 2001 to 2021.</p>
<p>It&rsquo;s incredibly sad that today, just a few years later, Ruby Central has zero upcoming conferences for the first time in their 25 year existence. It&rsquo;s even more sad that this entire dispute, far from strengthening Ruby Central as they claimed to want, has instead attacked the very <a href="https://nesbitt.io/2025/12/22/package-registries-are-governance-as-a-service.html">community governance they were supposed to be providing</a>.</p>
<p>Even beyond getting thrown out of the project that I helped create, after fundraising for a decade to keep it alive, Ruby Central&rsquo;s betrayal of the community hurts. It hurts to see not just me, but a whole team of maintainers driven away from the projects they have nurtured and developed for so many years. It hurts to hear that even after all of this, it was done for no real reason anyone can explain, and produced no real benefits that anyone can point at. That&rsquo;s no way to rebuild trust, or to support and grow a community.</p>
<p>The Ruby community deserves better.</p>
<p>We can create open source projects <a href="https://github.com/gem-coop/governance">governed openly by their maintainers</a>, like <a href="http://gem.coop">gem.coop</a>. We can build new tools that make using Ruby better for everyone, like <a href="https://rv.dev/">rv</a> or <a href="https://github.com/RubyElders/ruby-butler/">Ruby Butler</a>. While I have no interest in creating any more Ruby non-profits, I hope one day there will be Ruby community organizations where the directors are elected by the community, perhaps like the <a href="https://www.python.org/nominations/elections/">Python Software Foundation board</a>.</p>
<p>We can&rsquo;t return to the past, when Ruby Central&rsquo;s conferences were popular and amazing community events, and when Ruby Together supported open source projects without claiming to own them. Instead of spending our time trying to bring back the old days, let&rsquo;s work together to build Ruby a brighter future.</p>
]]></content></entry><entry><title type="html">jj sales pitch</title><link href="https://andre.arko.net/2026/07/13/jj-sales-pitch/"/><updated>2026-07-13T12:32:46-07:00</updated><id>http://andre.arko.net/2026/07/13/jj-sales-pitch/</id><content type="html"><![CDATA[<p>A new jj tutorial, summarizing a many of the reasons jj is so useful, and powerful, in just a few paragraphs.</p>
<blockquote>
<p>jj improves upon Git by achieving two opposing goals at the same time: it has a simpler mental model and command set, but simultaneously provides more powerful functionality. jj simplifies Git not by hiding complexity, but by using a better conceptual model.</p></blockquote>
<p>&ndash; <a href="https://evmar.github.io/jjtut/pitch/">Evan&rsquo;s Jujutsu Tutorial</a></p>
]]></content></entry><entry><title type="html">Meet Spinel</title><link href="https://andre.arko.net/2026/07/06/meet-spinel/"/><updated>2026-07-06T00:07:45-07:00</updated><id>http://andre.arko.net/2026/07/06/meet-spinel/</id><content type="html"><![CDATA[<p>Now that I&rsquo;ve been working on <a href="https://spinel.coop">Spinel Cooperative</a> for a full year already, I have finally managed to write the very first post I meant to write about the entire situation: What is Spinel? Who is Spinel? Why is Spinel? Read on to finally receive the answers to all these and questions and more.</p>
<p>Head over to the <a href="https://spinel.coop/blog/">Spinel Cooperative blog</a> to read <a href="https://spinel.coop/blog/meet-spinel/">Meet Spinel</a>.</p>
]]></content></entry><entry><title type="html">&lt;code>jj&lt;/code>'s whole deal</title><link href="https://andre.arko.net/2026/06/15/jjs-whole-deal/"/><updated>2026-06-15T00:00:00+00:00</updated><id>http://andre.arko.net/2026/06/15/jjs-whole-deal/</id><content type="html"><![CDATA[<blockquote>
<p>jj&rsquo;s whole deal is that it collapses many Git concepts (stashes, staging, fixups, in-progress rebases, conflicts) into a single unified model of working with history, which then lets you use the same tools to do all of those things. For example, to fix up an old commit you jump to it, edit it, and jump back to where you were; to fix a rebase conflict you jump to the conflicting commit, edit it, and jump back to where you were, using the same commands.</p></blockquote>
<p>&ndash; <a href="https://neugierig.org/software/blog/2025/08/jj-bookmarks.html">Understanding Jujutsu bookmarks</a></p>
<p>This is such a good summary, I&rsquo;m probably going to steal it when I try to explain <code>jj</code> in the future.</p>
]]></content></entry><entry><title type="html">&lt;code>rv&lt;/code> plan and progress update</title><link href="https://andre.arko.net/2026/06/13/rv-plan-and-progress/"/><updated>2026-06-13T00:00:00+00:00</updated><id>http://andre.arko.net/2026/06/13/rv-plan-and-progress/</id><content type="html"><![CDATA[<p><small>This post was originally given as a talk at <a href="https://rubycon.it">Rubycon IT 2026</a>. The <a href="https://speakerdeck.com/indirect/rv-a-ruby-manager-for-the-future-bd5a98e3-9989-444e-805f-b427a6591c09">slides</a> and <a href="https://www.youtube.com/watch?v=F8TZgUBwCYQ">video</a> are also available.</small></p>
<iframe class="speakerdeck-iframe" style="border: 0px; background: padding-box rgba(0, 0, 0, 0.1); margin: 0px; padding: 0px; border-radius: 6px; box-shadow: rgba(0, 0, 0, 0.2) 0px 5px 40px; width: 100%; height: auto; aspect-ratio: 560 / 315;" frameborder="0" src="https://speakerdeck.com/player/5432fb8785de421dbbb7a13841fd99ca" title="rv, a ruby manager for the future" allowfullscreen="true" allow="web-share" data-ratio="1.7777777777777777"></iframe>
<p>It&rsquo;s been a while since I first talked about <a href="/2025/09/03/rv-a-ruby-manager-for-the-future"><code>rv</code>, a Ruby manager for the future</a>. I&rsquo;d like give an update on what we&rsquo;ve done since then, but I&rsquo;m going to recap some of that earlier post first to give context for the updates. If you still remember what I said back then, you can <a href="#new-stuff">jump to the new stuff right away</a>. Either way, I&rsquo;m excited to update you about the work that we&rsquo;ve been doing, and show exactly how far we&rsquo;ve gotten.</p>
<h3 id="bundler-isnt-enough">bundler isn&rsquo;t enough</h3>
<p>For the last ten years or so of working on Bundler, I’ve had a wish rattling around: I want a bigger, better dependency manager. It doesn’t just manage your gems, it manages your ruby versions, too. It doesn’t just manage your ruby versions, it installs pre-compiled rubies so you don’t have to wait for ruby to compile from source over and over. And more than all of that, it makes it completely trivial to run any script or tool written in ruby, even if that script or tool needs a different ruby and gems than your application does.</p>
<p>For the entire ten years of daydreaming, I’ve been hoping someone else would build it and I could just use it. Then I discovered that someone <em>did</em> build it… but for Python. It’s called <code>uv</code>. In August 2024, uv version 0.3 shipped, and it had all the features I had wished for, and even more that I hadn’t thought to wish for.</p>
<p>At this point, I’ve been using <code>uv</code> for almost a year and every time I use a project written in Python, the experience is delightful. Not only can you run a command directly out of packages that aren&rsquo;t even installed, you can run a command that requires a Python version you don’t even have installed. <code>uv</code> takes care of installing the right python, installing the right packages, and running your command, in just a second or two.</p>
<p>Whether you want to run a CLI tool, a webapp, or a random script, <code>uv</code> always ensures the environment is correct as part of running the command. Need Python? Installed. Need a package? Also installed. Never again run <code>pip install</code> on a new package, only to realize later you broke something old. No more setting up dependencies manually, only to discover later that the script stopped working inside cron while you weren&rsquo;t checking on it.</p>
<h3 id="spinel-cooperative">spinel cooperative</h3>
<p>Last year, my long time consulting job disappeared and I found myself looking for something to replace it. One of my ideas was to start a company inspired by <a href="https://geomys.org">Geomys</a> in the Go language, offering expert advice from open source maintainers, but the idea felt weak to me without a “spotlight” project to show off our expertise. In July of this year, I finally realized that these two ideas could go together extremely well—our company can show our expertise by building this developer tool, and clients paying for our advice to solve their problems can ensure we are able to support and expand the tool.</p>
<p>I talked to some Ruby friends about the idea, and it resonated with them, so we started working on both the company and the open source project. Today, Spinel Cooperative has a website at <a href="https://spinel.coop">spinel.coop</a>, and <code>rv</code> has a website at <a href="https://rv.dev">rv.dev</a>. The team has expanded, and now includes <a href="https://github.com/deivid-rodriguez">David Rodriguez</a>, the former lead developer of RubyGems and Bundler, as well as former Rails core team members <a href="https://kaspth.com">Kasper Timm Hanson</a> and <a href="https://sls.name">Sam Stephenson</a>. Sam has even done some of this work before, as the original creator of <a href="https://rbenv.org"><code>rbenv</code></a> and the <code>ruby-build</code> tool.</p>
<h3 id="the-end-goal">the end goal</h3>
<p>Our goal for <code>rv</code> is to be a new kind of developer tool. You don’t need to install <code>rvm</code> and then pick a Ruby version, install it, and then update RubyGems and Bundler, and then <code>bundle install</code> your gems. Instead, you just run the project command you care about, and everything is handled. It&rsquo;s a version manager, and a dependency manager, and more than both of those things. With that vision in place, we were faced with a very practical question: what can we build that would be useful right away? After some prototyping and a lot of discussion, we landed on precompiled rubies for development work as the most useful place to start, and got to work.</p>
<p>After deciding what our first feature would be, we had to pick a language to use. We landed on Rust to build <code>rv</code>, for two main reasons. The obvious reason is that Rust produces very fast results, and that seems to also be why <code>uv</code> is written in Rust. The less obvious reason is based on years of trying to onboard new contributors to Bundler and RubyGems—it turns out if you are a Ruby developer, you unfortunately don&rsquo;t (yet) know the subset of Ruby that we have been forced to use to write Bundler and RubyGems.</p>
<p>There are two major things that basically every Ruby program does that you can&rsquo;t do if you are managing gems. First, you can&rsquo;t use any gems. If you want to use code that&rsquo;s inside a gem, you need to copy that code wholesale into Bundler or RubyGems, and then you need to constantly update it anytime that gem has any changes. Second, you can&rsquo;t use anything with native extensions, ever. JSON gem? Psych gem for YAML? Completely impossible, because Bundler and RubyGems need to be installable even if there is no compiler present.</p>
<p>So with those constraints in mind, and with our goal set to &ldquo;a tool so fast you normally can&rsquo;t even tell it&rsquo;s running&rdquo;, we settled on Rust, and started building a CLI. I&rsquo;ve used Rust for smaller personal projects in the past, but I had never created a full CLI tool. I am happy to report that the <code>clap</code> library for creating CLIs in Rust is great, and I recommend it to anyone interested in Rust CLIs.</p>
<h3 id="pre-compiling-ruby">pre-compiling ruby</h3>
<p>The next piece that we needed was the actual precompiled Rubies themselves. To install Ruby quickly, we needed to be able to skip over the <code>configure &amp;&amp; make &amp;&amp; make install</code> dance. There are a couple of big projects out there compiling Ruby in advance, but they are mostly for use on servers. The <code>setup-ruby</code> GitHub action, and the official Ruby docker images are both based on the <code>ruby-build</code> project originally started as part of <code>rbenv</code>. Unfortunately, those aren&rsquo;t usable for our needs because they aren&rsquo;t <strong>statically compiled</strong> and <strong>relocatable</strong>. Statically compiled (as opposed to dynamically compiled) means that Ruby copies the code from a shared library into its own binary.</p>
<p>Now for small aside (but it&rsquo;s relevant, I promise). Have you ever had trouble compiling Ruby because of OpenSSL? I&rsquo;m pretty sure every Ruby developer has. Have you ever had an already-installed Ruby suddenly stop working because of OpenSSL, and you had to install it again? That also seems extremely common, thanks to Homebrew&rsquo;s aggressive auto-update policy. The good news is, <code>rv</code> fixes both of those problems. By putting OpenSSL inside the Ruby binary, they can never get separated, and those errors can never occur.</p>
<p>There is a tradeoff here—if there is a critical security flaw in OpenSSL, we will need to compile Ruby again to include the critical security update. The first reason we are okay with this tradeoff is that OpenSSL doesn&rsquo;t have huge security issues very often. The second reason we are okay with this is that your production servers are probably using the official Ruby docker images and not Ruby installed by <code>rv</code>, so it&rsquo;s even less of a concern.</p>
<p>In the end, the closest existing system we were able find was Homebrew&rsquo;s <code>portable-ruby</code> project. That&rsquo;s how Homebrew builds the Ruby install that Homebrew itself runs on. The Homebrew team built some excellent infrastructure for building a statically linked Ruby, including libyaml, openssl, and other required libraries. The big thing Homebrew did not do was build more than one single version of Ruby, or support YJIT. We&rsquo;ll come back to that in a bit.</p>
<p>The <code>portable</code> part of <code>portable-ruby</code> is about builds being relocatable. Since Homebrew needs to be able to install into <code>/usr/local</code> on x86, but <code>/opt/homebrew</code> on Apple Silicon, and into any user&rsquo;s home directory for Linuxbrew, they need to be able to take a single precompiled Ruby and put it in any location on disk. That&rsquo;s another one of the requirements that isn&rsquo;t met by the <code>setup-ruby</code> or Docker image Rubies—if you move them to another directory, they stop working.</p>
<p>Using Homebrew&rsquo;s <code>portable-ruby</code> as a base, we were able to start with macOS ARM and Ubuntu x86, add Ubuntu on ARM, and then build every version in the Ruby 3.4.x series. Once we had those ready, then we asked ourselves: how much tooling do we need before this is useful for developers? Just linking to a repo with Ruby binaries in it isn&rsquo;t really that helpful, because if it&rsquo;s harder to use than running <code>rbenv install</code>, it&rsquo;s not really a better or faster experience.</p>
<p><a id="new-stuff"></a></p>
<h3 id="v01-proof-of-concept">v0.1 proof of concept</h3>
<p>We landed on a small set of useful features for the first version: the latest Ruby minor version, 3.4, built for macOS ARM and Linux x86, with support for <code>.ruby-version</code> files, and automatic Ruby version switching just in zsh.</p>
<p>After a few weeks of work, <code>rv</code> could switch between installed Ruby versions in zsh, but most importantly it could install precompiled Ruby on macOS and Ubuntu in one second flat. Yes, you heard that right. <code>rv ruby install</code>. Wait 1 second. Done. You can run Ruby commands now. With that functionality in place, we released version 0.1.</p>
<p>Immediately after our initial release, we were hit with an extremely nice surprise: someone from the Homebrew core team decided to add <code>rv</code> directly to homebrew-core within a few days of <code>rv</code> 0.1 being released. That makes it much easier to install <code>rv</code> and try it out, and completely removes any need for us to create and maintain our own custom homebrew tap, which is a very nice bonus.</p>
<h3 id="adding-shells-rubies-yjit-and-tools">adding shells, rubies, YJIT, and tools</h3>
<p>With proof our concept working and users installing v0.1, we immediately started to expand the core functionality. We added support for bash, fish, and nushell. We spent several weeks working through the issues involved in compiling every single point release of Ruby 3.3 and 3.4. Then we spent another two weeks working through all of the issues compiling all of those Rubies with YJIT turned on. Then we spent another two weeks working through the issues of compiling all of those Rubies for macOS on x86, and for Linux on ARM. Once all of those Ruby versions were available, we shipped version 0.2.</p>
<p>Building on our progress with Ruby versions, we added more versions of Ruby: every 3.2.x version, and all of the 4.0 prereleases and final releases. After hearing from <code>asdf</code> and <code>mise</code> users who wanted to re-use their <code>.tool-versions</code> file, we added support for that file as well. Automatic Ruby switching will respect <code>.tool-versions</code> files, and <code>rv ruby pin</code> will update the version written into the <code>.tool-version</code> file if it exists. As a fun easter egg, we also added a precompiled binary of the oldest version of Ruby with published source code, 0.49. All of those features shipped as <code>rv</code> version 0.3.</p>
<h3 id="what-does-it-mean-to-manage-gems">what does it mean to manage gems?</h3>
<p>At that point, we took a break to take stock of the project, our goals, and our plan. <code>rv</code> 0.3 is a pretty good Ruby version manager, and a viable option in the pantheon of Ruby version managers like <code>rvm</code>, <code>rbenv</code>, or <code>chruby</code>. While precompiled Ruby is great, we want superfast installs for not just Ruby but also all gems and bundles. But Bundler is huge! It took three of us a year to build originally, and has had 15 years of additions by dozens of contributors. We can&rsquo;t build everything we want in a month, or even three. After much brainstorming and discussion, we made a plan to deliver real-world useful tools that would build on each other, so we can work our way up to a complete application dependency management tool.</p>
<p>First, we would need to understand gems themselves, parsing the compact index of gem metadata and then reading gemspecs and .gem files. Then we would need to install gems, not just copy files into the right places but also running the steps to compile native extensions correctly. Once we can install gems into the right places, we need to parse the <code>Gemfile.lock</code> format to install bundles. Then we need to build a resolver, the process that transforms a <code>Gemfile</code> into a <code>Gemfile.lock</code> by taking a list of gems and producing a graph of dependencies that are all compatible with each other. With that plan, we got back to work.</p>
<p>I also want to call out the non-Spinel contributors to <code>rv</code> here. The community that formed around <code>rv</code> and started contributing to it has been amazing. In particular, I want to thank <a href="https://github.com/segiddins">Samuel Giddins</a> for work on gem and gemspec parsing and Ruby shell integration, <a href="https://github.com/adamchalmers">Adam Chalmers</a> for work on parsing Gemfile.lock and installing gems, and <a href="https://github.com/case">Eric Case</a> for work on Windows and Alpine compatibility. We wouldn&rsquo;t have made it anywhere close to this far without all of your help.</p>
<h3 id="the-clean-install-command-windows-powershell-and-alpine">the <code>clean-install</code> command, windows, powershell, and alpine</h3>
<p>The first feature from that plan was <code>clean-install</code>, which does the same thing as <code>bundle install --frozen</code>. This is the same thing that you use when you&rsquo;re running your tests in CI, or that you use when you&rsquo;re deploying your application to a server. As long as you haven&rsquo;t made any changes to your Gemfile, we can read the lockfile, install all of your gems, and set up the environment so that your application is able to run. To build this, we implemented a compact index client, gemspec parsing, native gem extension compilation, and gem installation. And it works! Starting with <code>rv</code> 0.4, you can clone a project, install your gems, and run the project.</p>
<p>The next release included a small sidequest to add Windows and PowerShell support, as well as compiling Ruby binaries against musl libc so they will work on Alpine Linux. We use the precompiled binaries for Windows produced by the ruby-installer project, which turns out to be the only existing project that precompiles Ruby. This release also included the next two steps of our incremental plan: first, automatically managing Ruby version and installation. If you <code>rv run irb</code>, you don&rsquo;t even need to have Ruby installed, <code>rv</code> will make sure that happens if needed.</p>
<h3 id="tool-management-rvx-and-ruby-dev">tool management, <code>rvx</code>, and ruby-dev</h3>
<p>The second part was the next step of our gem management plan, taking a list of gems and resolving dependencies to install. When combined, those two features unlock uv-style &ldquo;tools&rdquo;, where a gem CLI can also have an auto-managed Ruby version. Have you ever used <code>gem install</code> to get a CLI tool only to find out later your Ruby version changed and broke the CLI? <code>rv</code> tools completely prevent that problem. With tool support, we could then add gem auto-install to create <code>rvx</code>. Run any gem command, even if it&rsquo;s not installed! With <code>rv</code> version 0.5, you can go straight from <code>brew install rv</code> to a Rails app from <code>rvx rails new</code> in 10 seconds flat.</p>
<p>At the SF Ruby conference late last year, a random conversation with Kokubun, the ruby-core member and maintainer of YJIT and ZJIT spawned an idea: what about testing against the latest Ruby? The Ruby version managers that compile Ruby onto your own machine handle this by adding a version of Ruby named &ldquo;dev&rdquo; that just means &ldquo;check out the ruby git repo and compile the newest commit&rdquo;. It was only a few days of effort to get the <code>rv</code> ruby compiler handling ruby from git, but it was a few weeks of experimenting before figuring out how to handle a &ldquo;version&rdquo; that keeps the same name but changes every day. It was worth it, though, because now you can install and test against the latest daily Ruby build as easily and as often as you want, without ever waiting for Ruby to compile.</p>
<h3 id="coming-up-next">coming up next</h3>
<p>It&rsquo;s not quite finished yet, but the next step in our incremental plan is to handle the same responsibilities that the <code>bundle install</code> command handles: evaluate the Gemfile, resolve the graph of gem versions, update the Gemfile.lock if needed, and install all of those gems. When I was learning about uv, this part absolutely blew my mind because <code>uv sync</code> is so fast that it runs as part of every command! Coming from Bundler, that was completely incredible. I could not imagine running <code>bundle install</code> before every <code>bundle exec</code> because that would make everything so, so slow. It&rsquo;s very exciting to work toward that for Ruby.</p>
<p>That&rsquo;s not all we have planned, either. The <code>rv</code> roadmap includes project setup and task management, making it easy to run scripts or other commands with your Ruby and gems available. Managing gems for scripts means adding a config file as a comment inside the ruby script file, with the Gemfile-like information needed to install gems. <code>rv</code> can then auto-install those gems in order to run the script. It&rsquo;s not yet clear how long it will take to finish this initial list, even after it&rsquo;s done we have a ton of additional ideas.</p>
<p>As we wind things up, I want to show off a couple of things that I personally think are the best and coolest uses of rv. this isn&rsquo;t necessarily the stuff that you&rsquo;ll do the most often, which is fine, but these examples are super impressive to me, coming from the nightmare of ruby version building.</p>
<h3 id="example-uses">example uses</h3>
<p>First up, <code>rvx</code>: once you have <code>rv</code>, you don&rsquo;t need to think about Ruby, you don&rsquo;t need to think about gems, you just run the command that you want to run, immediately. <code>rv</code> is fast enough that you can start on a machine with no Ruby installed, run <code>rvx rails new .</code>, and be generating that app in less than 10 seconds. One command to install Ruby, install Rails, install all 60 gems that Rails depends on, and run the command you originally wanted. It&rsquo;s just an incredibly delightful experience to not need to think about Ruby versions or gem dependencies when you want to run something.</p>
<p>Another thing that has come extremely in handy is the ability to write scripts across Ruby versions, and know those scripts will work whether or not those Ruby versions are installed when the script runs. You don&rsquo;t need to care about installing Ruby, or even checking for Ruby at all. Just run the command you want to run and <code>rv</code> will take care of all that stuff.</p>
<pre><code># share scripts using many rubies, all auto-installed
$ rv run --ruby 3.3 bench.rb
$ rv run --ruby 3.4 bench.rb
$ rv run --ruby 4.0 bench.rb
</code></pre>
<p>Finally, the <code>rv tool</code> commands (inspired by <code>uv tool</code>) allow you to use CLIs without having to think about Ruby versions, or global gems, or bundled gems, or what application directory you are in. Tools always get the Ruby version and the gems that they need to work, regardless of your currently chosen Ruby version and app and gems. For me, <code>rv tool</code> has unlocked the ability to use Ruby CLI tools again, and I love that power and flexibility.</p>
<pre><code># CLIs outisde of app Ruby versions and gems
$ rv tool install gist
$ cd ruby-4-app
$ gist
$ cd ../ruby-2-app
$ gist
</code></pre>
<p>In the end, we want to live in a future where anyone can run a Ruby command, or tool, or application in a few seconds (or less!). We&rsquo;re building that future for ourselves, and we welcome everyone else. Visit <a href="https://rv.dev">rv.dev</a> to see the project on GitHub and give it a try! We&rsquo;d love to have your help building it.</p>
]]></content></entry><entry><title type="html">Software developers have become their own joke</title><link href="https://andre.arko.net/2026/04/13/software-developers-have-become-their-own-joke/"/><updated>2026-04-13T01:17:52-07:00</updated><id>http://andre.arko.net/2026/04/13/software-developers-have-become-their-own-joke/</id><content type="html"><![CDATA[<p>Creating software is complicated. It&rsquo;s hard to figure out exactly what you need to build without a lot of trial and error. It almost always requires both exploring possible options <em>and</em> refining something until it works really well. But those things aren’t the same! Your research prototype is <em>not</em> a good product that people will happily pay for.</p>
<p>Back in the olden days, when software literally came from BigCo R&amp;D departments, we managed to invent Unix, and the mouse, and GUIs, and Ethernet, and TCP/IP, and a ton of other stuff we all use constantly today. Those research divisions didn&rsquo;t ship viable consumer products, though. Doug Englebart demoed a mouse-driven GUI in 1968, but you couldn&rsquo;t buy a home computer with a mouse-driven GUI until 1979, and they didn&rsquo;t become commercially popular until the Macintosh in 1984.</p>
<p>Even years or decades of research wasn&rsquo;t enough, and years (or decades!) of development work also needed to be done before the results was ready for people to use. Early literature about creating software, written by Fred Brooks and his peers, seems to contain the internalized view that both R and D are required. That&rsquo;s not surprising, since R&amp;D departments created most software back then, but we seem to have lost track of that connection.</p>
<p>Even though our jobs are descended from those R&amp;D labs of yore, we somehow lost the industry job of “software researcher”, and only &ldquo;software developer&rdquo; remains. Instead, research happens in academia, where an argument and some pseudocode is all you need to publish a paper. In that world, development is effectively non-existent.</p>
<p>(I admit the division isn&rsquo;t perfectly clear-cut. Sometimes academics will start companies around their research that create a product, or more likely get acquired to add a feature to a product. And sometimes Linus Torvalds will just build a new operating system, without doing any academic research on it, and it will get so popular everyone uses it. The point is that industry and academia have each publicly claimed one half of R&amp;D while disowning the other.)</p>
<p>The broader separation of research and development into academia and industry is really unfortunate, because good software needs both research <em>and</em> development as inputs. If you don&rsquo;t do any research, you can’t identify which parts will be hard (or impossible) until after it’s too late. You also won’t have a good idea of what parts are important until after you’ve put in most or all of the work to create the parts that don’t matter. If you don&rsquo;t do development, you won&rsquo;t ever have something robust enough that other people can use it successfully.</p>
<p>Meanwhile, on the other side, it feels like developers work hard to convince themselves there are no research aspects involved in their jobs. We call anything research-ish by another name, like “design”, “user experience”, “prototyping”, “de-risking”, “a spike”, and a lot of other funny euphemisms that avoid referring to the work as research. It seems like we&rsquo;re trying to convince ourselves that we don’t do Research any more, because we are just Developers.</p>
<p>This cultural lack of clarity around research in software development spaces really hit hard for me this week, as I read yet another treatise on working with LLM-driven agents for development. The two most popular takes that I have seen are “these tools are a fundamental shift in the nature of software development” and “these tools change nothing about building software at all”. Then the two sides start screaming at each other about how the other side is delusional and time will prove them completely wrong, and I lose interest.</p>
<p>If we instead start from the premise that all software work requires research (where the problem space must be explored) and development (where solutions must be implemented and refined), there’s something hiding in the sometimes messy overlap between those two ideas that I&rsquo;m not seeing come up in any discussions.</p>
<p><strong>No one can take the output of software research and treat it like it&rsquo;s the output of software development.</strong> Not Bell Labs, not Xerox PARC, not Microsoft middle managers, and not &ldquo;solo founders managing a team of AI agents&rdquo; today.</p>
<p>Unfortunately, seeing a prototype and becoming convinced it&rsquo;s complete is not a new problem. It&rsquo;s been the bane of software development possibly since the very beginning, when (apocryphally) a manager would review a mockup and conclude the project was now complete and could be shipped to customers immediately. Today, instead of telling that story as a joke, software developers have have somehow turned themselves into the boss from the joke, shouting that it&rsquo;s time to ship the research prototype because it &ldquo;looks finished&rdquo;. How did we do this to ourselves?</p>
<p>It seems like, back when we always had to do all the work ourselves, it was harder for software developers to be confused this way. If a developer knows they skipped every validation and edge case, it&rsquo;s much easier to realize it&rsquo;s not finished. If an LLM agent says &ldquo;here&rsquo;s a comprehensive implementation&rdquo;, without mentioning all the validations and edge cases it skipped, many (and possibly most) developers will not notice the parts that are missing.</p>
<p>This phenomenon is bad for a lot of reasons, including one reason you have probably already thought of: we’re going to get a lot more software claiming to be &ldquo;comprehensive&rdquo; and &ldquo;fully implemented&rdquo; when it’s really a partially finished prototype that’s full of holes.</p>
<p>In a world full of research prototypes being pitched as completed development work, life is about to get worse for everyone who uses software. The docs are even more wildly wrong than they were before, customer support is telling you that your problem is solved by a feature that doesn&rsquo;t exist, and company leadership is so excited they are planning to fire as many humans as possible so they can have more of it.</p>
<p>I don&rsquo;t want worse software! The software we already have is mostly terrible. Not only much worse software, but also much more of it, is pretty much my worst case scenario. What I actually want is better software, even if that means less of it.</p>
<p>Unfortunately, instead of making better software, software developers have decided to become the butt of their own joke, shipping software that doesn&rsquo;t work, with a footnote that says they know it doesn&rsquo;t work but they are still shipping it. I don&rsquo;t see any way to stop it, but I hate it anyway.</p>
]]></content></entry><entry><title type="html">Towards an Amicable Resolution with Ruby Central</title><link href="https://andre.arko.net/2026/04/02/towards-an-amicable-resolution-with-ruby-central/"/><updated>2026-04-02T11:06:38-07:00</updated><id>http://andre.arko.net/2026/04/02/towards-an-amicable-resolution-with-ruby-central/</id><content type="html"><![CDATA[<p>Last week, three members of Ruby Central’s board published <a href="https://rubycentral.org/news/a-message-from-the-ruby-central-board/">a new statement about RubyGems and Bundler</a>, and this week they published <a href="https://rubycentral.org/news/rubygems-fracture-incident-report/">an incident report on the events last year</a>. The first statement reports that Ruby Central has now completed a third audit of RubyGems.org’s infrastructure: first <a href="https://github.com/ossf/alpha-omega/blob/main/alpha/engagements/2025/Ruby%20Central/update-2025-10.md#unauthorized-access-and-credentials-rotation">by the sole remaining RubyGems.org maintainer</a>, the second <a href="https://github.com/ossf/alpha-omega/blob/main/alpha/engagements/2026/Ruby%20Central/update-2026-01.md#aws-security-audit">by Cloud Security Partners</a>, and the third by Hogan Lovells.</p>
<p>In all three cases, Ruby Central found <a href="https://rubycentral.org/news/rubygems-org-aws-root-access-event-september-2025/">no evidence of compromised end user data, accounts, gems, or infrastructure availability</a>. I hope this can conclusively put to rest the idea that I have any remaining access to the RubyGems.org production systems, or that I caused any harm to the RubyGems.org service at any time.</p>
<p>I also appreciate that Ruby Central is taking its share of responsibility, recognizing that its lack of communication with the former maintainers (including me) created confusion and frustration that contributed, in part, to how we ended up where we are today.</p>
<p>Ruby Central board members Freedom, Brandon, and Ran state that their intent is now to work towards an amicable resolution. I salute their new commitment, and would like to do my part to help the RubyGems community move past these unfortunate events, with a resolution that puts the dispute fully behind us, and allows all of us to move forward.</p>
<p>For my part, despite my claims against Ruby Central, and the threats they have directed against me, I am willing to completely settle all of my disputes with them, and pledge to take no legal action against Ruby Central regarding any of their actions prior to today.</p>
<p>In exchange, I am requesting two things.</p>
<p>First, I am asking Ruby Central to drop their legal threats, including releasing their claims against me and reimbursing my legal costs. Those costs arise from Ruby Central’s actions, including litigation threats, other escalations, and most recently contacting law enforcement. In addition to forcing me to retain counsel, these actions caused considerable stress and disruption. I am willing to provide invoices to ensure the reimbursement precisely matches only my actual costs.</p>
<p>Second, I am asking Ruby Central lay our disagreement to rest with a public statement acknowledging that I did no harm to the RubyGems.org service.</p>
<p>If Ruby Central fully drops their legal claims, and states I did not harm the RubyGems.org service, I would consider our disagreement amicably settled.</p>
]]></content></entry><entry><title type="html">How to Install a Gem</title><link href="https://andre.arko.net/2026/03/24/how-to-install-a-gem/"/><updated>2026-03-24T18:55:18-07:00</updated><id>http://andre.arko.net/2026/03/24/how-to-install-a-gem/</id><content type="html"><![CDATA[<p><small>This post was originally given as a talk at <a href="https://sfruby.com">SF Ruby Meetup</a>. The <a href="https://speakerdeck.com/indirect/how-to-install-a-gem/">slides</a> are also available.</small></p>
<iframe class="speakerdeck-iframe" style="border: 0px; background: padding-box rgba(0, 0, 0, 0.1); margin: 0px; padding: 0px; border-radius: 6px; box-shadow: rgba(0, 0, 0, 0.2) 0px 5px 40px; width: 100%; height: auto; aspect-ratio: 560 / 315;" frameborder="0" src="https://speakerdeck.com/player/3bf2a60d63cb4a40a23c0e0dccb6a601" title="How to install a gem" allowfullscreen="true" data-ratio="1.7777777777777777"></iframe>
<h3 id="its-more-complicated-than-it-sounds">It&rsquo;s more complicated than it sounds</h3>
<p>Hello, and welcome to <em>How To Install A Gem</em>. My name is André Arko, and I go by @indirect on all the internet services. You might know me from being 1/3 of the team that shipped Bundler 1.0, or perhaps the 10+ years I spent trying to keep RubyGems.org up and running for everyone to use.</p>
<p>More recently, I&rsquo;ve been working on new projects: <a href="https://rv.dev"><code>rv</code></a>, a CLI to install Ruby versions and gems at unprecedented speeds, and <a href="https://gem.coop">gem.coop</a>, a community gem server designed from the ground up so Bundler and <code>rv</code> can install gems faster and more securely than ever before.</p>
<p>So, with that introduction out of the way, let’s get started: do you know how to install a gem? Okay, that’s great! You can come up and give this talk instead of me. I’ll just sit over here while you write the rest of this post.</p>
<p>Slightly more seriously, do you know how <code>gem install</code> converts the name that you give it into a URL to download a .gem file? It’s called the “compact index”, and we’ll see how it works very soon.</p>
<p>Next, who in the audience knows how to unpack a .gem file? Do you know what format .gem files use, and what&rsquo;s inside them? We’ll look at gem structure and gemspec files as well.</p>
<p>Then, do you know where to put the files from inside the gem? Where do all of these files and directories get put on disk so we can use them later? Does anyone know off the top of their head?</p>
<p>Once those files have been unpacked into the correct places, the last thing we need to know is how to require them. How do these unpacked files on disk get found by Ruby, so you can <code>require &quot;rails&quot;</code> and have that actually work?</p>
<p>This exercise was mostly to show that using gems every day actually skips over most of the way they work underneath. So let’s look at what a gem is, and examine how they work. By the end of this talk, you’ll know what’s inside a gem, how RubyGems figures out what to download, and where and how that download gets installed so you can use it.</p>
<p>And if you already everything we just talked about, please feel free to go straight to <a href="https://rv.dev">rv.dev</a> and start sending us pull requests!</p>
<h3 id="how-does-a-name-become-a-gem-url">How does a name become a .gem URL?</h3>
<p>First, we&rsquo;re going to look at how the name of a gem becomes a URL for a .gem file. Let&rsquo;s use <code>rails</code> as our example. Historically, there have been at least five or six different ways to look up information about a gem based on its name, but today there is one canonical way: the compact index. It&rsquo;s so simple that you can do it yourself using curl. Just run <code>curl https://gem.coop/info/rails</code>, and you&rsquo;ll be able to read the exact output that every tool uses to look up the versions of a gem that exist. Each line in the file describes one version of the gem, so let&rsquo;s look at one line.</p>
<pre><code>❯ curl -s https://gem.coop/info/rails | tail -n 1
8.1.3 actioncable:= 8.1.3,actionmailbox:= 8.1.3,actionmailer:= 8.1.3,actionpack:= 8.1.3,actiontext:= 8.1.3,actionview:= 8.1.3,activejob:= 8.1.3,activemodel:= 8.1.3,activerecord:= 8.1.3,activestorage:= 8.1.3,activesupport:= 8.1.3,bundler:&gt;= 1.15.0,railties:= 8.1.3|checksum:6d017ba5348c98fc909753a8169b21d44de14d2a0b92d140d1a966834c3c9cd3,ruby:&gt;= 3.2.0,rubygems:&gt;= 1.8.11
</code></pre>
<p>We can break down that line with <code>split(&quot; &quot;)</code>, and tackle each part one at a time.</p>
<p>First, <code>8.1.3</code>. That&rsquo;s the version of <code>rails</code> that this line is about. So we now know for sure that <code>rails 8.1.3</code> exists.</p>
<p>Next, a list of dependencies. The <code>rails</code> gem (version <code>8.1.3</code>) declares dependencies on a bunch of other gems: <code>actioncable</code>, <code>actionmailbox</code>, <code>actionmailer</code>, <code>actionpack</code>, <code>actiontext</code>, <code>actionview</code>, <code>activejob</code>, <code>activemodel</code>, <code>activerecord</code>, <code>activestorage</code>, <code>activesupport</code>, <code>bundler</code>, and <code>railties</code>. Each dependency has a version requirement attached, and for almost every gem it is exactly version <code>8.1.3</code>, and only version <code>8.1.3</code>. For <code>bundler</code>, Rails is a little bit more flexible, and allows any version <code>1.15.0</code> and up.</p>
<p>The final section contains a checksum, a ruby requirement, and a rubygems requirement. The checksum is a sha256 hash of the .gem file that contains the gem, so after we download the gem we can check to make sure we have the right file by comparing that checksum.</p>
<p>For this version of Rails, the required Ruby version is <code>3.2.0</code> or greater, and the required RubyGems version is <code>1.8.11</code> or greater. It&rsquo;s up to the client to do something with that information, but hopefully you&rsquo;ll see an error if you are using Ruby or RubyGems that&rsquo;s too old.</p>
<p>Great! So now we know the important information: Rails version <code>8.1.3</code> is real, and strong, and is our friend. We can download it, and check the checksum against the checksum we were given in the info file line. Let&rsquo;s do that now:</p>
<pre><code>❯ curl -sO https://gem.coop/gems/rails-8.1.3.gem
❯ sha256sum rails-8.1.3.gem
6d017ba5348c98fc909753a8169b21d44de14d2a0b92d140d1a966834c3c9cd3  rails-8.1.3.gem
</code></pre>
<p>Notice that the checksum produced by <code>sha256sum</code> exactly matches the checksum we previously saw in our line from the info file: <code>6d017ba5348c98fc909753a8169b21d44de14d2a0b92d140d1a966834c3c9cd3</code>. That lets us know that we got the right file, and there were no network or disk errors.</p>
<h3 id="what-exactly-is-in-a-gem">What exactly is in a gem?</h3>
<p>Now that we have the gem, we can investigate: what exactly is inside a gem? At this point, we&rsquo;re going to pivot from the <code>rails</code> gem to the <code>railties</code> gem. There&rsquo;s a good reason for that, and the reason is&hellip; the <code>rails</code> gem doesn&rsquo;t actually have any files in it. So it&rsquo;s a bad example. In order to show off what a gem looks like when it has files in it, we&rsquo;ll use <code>railties-8.1.3.gem</code> instead.</p>
<p>So, we have our .gem file downloaded with curl. What do we do now? The first piece of secret knowledge that we need: gems are tarballs. That means we can open them up with regular old <code>tar</code>. Let&rsquo;s try it.</p>
<pre><code>❯ tar xfvz railties-8.1.3.gem
x metadata.gz
x data.tar.gz
x checksums.yaml.gz
</code></pre>
<p>So what&rsquo;s inside the .gem tarball is&hellip; another tarball. And also two gzipped files. Let&rsquo;s look at the files first.</p>
<pre><code>❯ gzcat checksums.yaml.gz
---
SHA256:
  metadata.gz: 8326ab6cc8e325055394ebd19c41d895e9ebd48e4752ec90d5c4675935516e6e
  data.tar.gz: 4152d8f55ae639d899f1cb6c54e1e93bb158bb76026b253482c5ae0343ac5aec
SHA512:
  metadata.gz: f6aa3390b6b1699255f1dbc6c6f24c6d9c18d3bfa48f10b6d720595384b4d1bb26f92232adb7011d3b1e7e977ca775cd253a12a135fe83eaa21e10dd0f14f779
  data.tar.gz: a001cebc5b97f627336a3e8d394c4ecac4a5d2b9e62c82de3b484470a58deac7c8ff0a8e8b497843386b1639c9cbfdfabee2cc7b2d483469e00a0b01da6bd41d
</code></pre>
<p>As you might expect from its name, the <code>checksums.yaml.gz</code> file is a gzipped YAML file, containing checksums for the other two files. It&rsquo;s maybe a bit silly to have multiple layers of checksumming here, but it does confirm that the outer layer of tarball and zip was removed without any errors.</p>
<p>Okay, so what&rsquo;s inside <code>metadata.gz</code>? The answer is&hellip; Ruby, sort of. It&rsquo;s a YAML-serialized instance of the <code>Gem::Specification</code> class. We can see exactly what was put into this object at the time the gem was built.</p>
<p>After snipping out the YAML that lists the dependencies (which we already looked at, because they are included in the info file), what&rsquo;s left is some relatively simple information about the gem. Author, author&rsquo;s email, description, homepage, license, various URLs.</p>
<pre><code>❯ gzcat metadata.gz
--- !ruby/object:Gem::Specification
name: railties
version: !ruby/object:Gem::Version
  version: 8.1.3
platform: ruby
bindir: exe
executables:
- rails
require_paths:
- lib
authors:
- David Heinemeier Hansson
summary: Tools for creating, working with, and running Rails applications.
description: 'Rails internals: application bootup, plugins, generators, and rake tasks.'
email: david@loudthinking.com
homepage: https://rubyonrails.org
licenses:
- MIT
metadata:
  bug_tracker_uri: https://github.com/rails/rails/issues
  changelog_uri: https://github.com/rails/rails/blob/v8.1.3/railties/CHANGELOG.md
  documentation_uri: https://api.rubyonrails.org/v8.1.3/
  mailing_list_uri: https://discuss.rubyonrails.org/c/rubyonrails-talk
  source_code_uri: https://github.com/rails/rails/tree/v8.1.3/railties
  rubygems_mfa_required: 'true'
rubygems_version: 4.0.6
specification_version: 4
files:
- CHANGELOG.md
- MIT-LICENSE
- RDOC_MAIN.md
- README.rdoc
- exe/rails
- lib/minitest/rails_plugin.rb
- lib/rails.rb
[...]
</code></pre>
<p>For the purposes of installing and using the gem, we care about exactly six pieces of information: <code>name</code>, <code>version</code>, <code>platform</code>, <code>bindir</code>, <code>executables</code>, and <code>require_paths</code>.</p>
<p>We&rsquo;re going to combine those items with the files in the remaining <code>data.tar.gz</code> file to get our unpacked and installed gem.</p>
<h3 id="what-gets-unpacked-and-where">What gets unpacked, and where?</h3>
<p>Now that we know what&rsquo;s in the gem specification, let&rsquo;s look at what&rsquo;s inside the data tarball. It matches up very closely with the long list of entries in the <code>files</code> array in the gemspec.</p>
<pre><code>❯ mkdir railties-8.1.3
❯ tar xfvz data.tar.gz -C railties-8.1.3
x CHANGELOG.md
x MIT-LICENSE
x RDOC_MAIN.md
x README.rdoc
x exe/rails
x lib/minitest/rails_plugin.rb
x lib/rails.rb
[...]
</code></pre>
<p>So now we have a bunch of files. Where are we going to put these files? Enter: the magic of RubyGems. The scheme that RubyGems has come up with is largely shaped by the constraints of how Ruby finds files to require, which we&rsquo;re going to look at soon. For now, it is enough for us to know that RubyGems keeps track of a list of directories, a lot like the way <code>$PATH</code> works for your shell to find commands to run. To find the current directory, you can run <code>ruby -e 'puts Gem.dir</code>. Here&rsquo;s what that looks like:</p>
<pre><code>❯ ruby -e 'puts Gem.dir'
/Users/andre/.gem/ruby/4.0.0
❯ ls ~/.gem/ruby/4.0.0 | xargs -L1 echo
bin
build_info
cache
doc
extensions
gems
plugins
specifications
</code></pre>
<p>From this list, we can see that RubyGems organizes its own files into a few directories. To install a gem, we&rsquo;re going to need to put the files we have into each of those directories, with specific paths and filenames.</p>
<p>Just to recap, the files we need to place somewhere are:</p>
<ul>
<li>railties-8.1.3.gem (the .gem file itself)</li>
<li>metadata.gz (the YAML Gem::Specification object from inside the gem)</li>
<li>the unpacked data.tar.gz files (the contents of the gem)</li>
</ul>
<p>So let&rsquo;s move the files into the directories we see RubyGems offers.</p>
<p>First, cache the .gem file so RubyGems doesn&rsquo;t need to download it again later:</p>
<pre><code>mv railties-8.1.3.gem ~/.gem/ruby/4.0.0/cache/
</code></pre>
<p>Then, add the gem specification so that RubyGems will be able to find it. There&rsquo;s a small twist here, which is that the <code>specifications</code> directory doesn&rsquo;t contain YAML files, it contains Ruby files. So we also need to convert the YAML file back into a Ruby object, and then write out the Ruby code to create that object into a file that RubyGems can load later.</p>
<pre><code>❯ gunzip metadata.gz
❯ ruby -ryaml -e 'puts YAML.unsafe_load_file(&quot;metadata&quot;).to_ruby' &gt; ~/.gem/ruby/4.0.0/specifications/railties-8.1.3.specification
</code></pre>
<p>Next, we need to put the files that make up the contents of the gem into the <code>gems/</code> directory.</p>
<pre><code>❯ mv railties-8.1.3 ~/.gem/ruby/4.0.0/gems/
❯ ls ~/.gem/ruby/4.0.0/gems/railties-8.1.3
CHANGELOG.md
exe
lib
MIT-LICENSE
RDOC_MAIN.md
README.rdoc
</code></pre>
<p>One more thing we need to do: set up the executables provided by the gem.</p>
<p>You can check out the files that RubyGems generates by looking in <code>~/.gem/ruby/4.0.0/bin</code>, but for our purposes we just need to tell RubyGems what gem and executable it needs to run, so we can do that:</p>
<pre><code>❯ cat &lt;&lt;EOF &gt; ~/.gem/ruby/4.0.0/bin/rails
#!/usr/bin/env ruby
require &quot;rubygems&quot;
Gem.activate_and_load_bin_path(&quot;railties&quot;, &quot;rails&quot;)
EOF
❯ chmod +x ~/.gem/ruby/4.0.0/bin/rails
</code></pre>
<p>And with that, we&rsquo;ve installed the gem! You can run the <code>rails</code> file that we just created to prove it:</p>
<pre><code>❯ ~/.gem/ruby/4.0.0/bin/rails
Usage:
  rails COMMAND [options]

You must specify a command:

  new          Create a new Rails application. &quot;rails new my_app&quot; creates a
               new application called MyApp in &quot;./my_app&quot;
</code></pre>
<p>As we wrap up here, there are three aspects of gems that we haven&rsquo;t touched on at all: docs, extensions, and plugins. We don&rsquo;t have time to talk about them today in this meetup talk slot. Hopefully a future (longer) version of this talk will have space to include all of those things, because they are all super interesting, I promise.</p>
<p>In the meantime, I will have to direct you to the <a href="https://ruby.github.io/rdoc/">docs for RDoc</a> to learn more about docs, to <a href="https://github.com/spinel-coop/rv/blob/75786fe29c55452abfc725d43165a1b3035a552e/crates/rv/src/commands/clean_install.rs#L1504">the source code of <code>rv</code></a> or <a href="https://github.com/ruby/rubygems/blob/master/lib/rubygems/ext/ext_conf_builder.rb">RubyGems itself</a> if you want to learn more about gem extensions and plugins.</p>
<h3 id="how-does-require-find-a-gem">How does <code>require</code> find a gem?</h3>
<p>There&rsquo;s one last thing to figure out before we wrap up: how does <code>require</code> find a gem for us to be able to use it? To explain that, we&rsquo;ll have to drop down to some basic Ruby, and then look at the ways that RubyGems monkeypatches Ruby&rsquo;s basic <code>require</code> to make it possible to have gems with versions.</p>
<p>The first thing to know about <code>require</code> is that it works exactly like <code>$PATH</code> does in your shell. There&rsquo;s a global Ruby variable named <code>$LOAD_PATH</code>, and it&rsquo;s an array of paths on disk. When you try to require something, Ruby goes and looks inside each of those paths to see if the thing you asked for is there.</p>
<p>You can test this out for yourself in just a few seconds! Let&rsquo;s try it.</p>
<pre><code>❯ mkdir lib
❯ echo 'puts &quot;this is some ruby&quot;' &gt; lib/my-file.rb
❯ ruby -Ilib -e 'puts $LOAD_PATH.first; require &quot;my-file&quot;'
/Users/andre/lib
this is some ruby
</code></pre>
<p>The Ruby CLI flag <code>-I</code> lets you add directories to the <code>$LOAD_PATH</code> variable, and then the <code>require</code> function looks inside that directory to find a file with the name that you gave to require. No magic, just a list to check against for files on disk.</p>
<p>Now that you understand how the <code>$LOAD_PATH</code> variable makes <code>require</code> work, how does RubyGems work? You can&rsquo;t just put ten different versions of <code>rake</code> into the <code>$LOAD_PATH</code> and expect <code>require</code> to still work. RubyGems handles multiple versions of the same <code>rake.rb</code> file by monkeypatching <code>require</code>.</p>
<p>Let&rsquo;s look at what happens when we <code>require &quot;rails&quot;</code>, which is a file located inside the <code>railties</code> gem that we just installed. RubyGems starts by looking at all of the gem specifications, including the one we saved earlier. In each specification, it combines the name and version with the values in <code>require_paths</code> to come up with a path on disk.</p>
<p>So for our just-installed <code>railties</code> gem, that would mean a path of: <code>~/.gem/ruby/4.0.0/gems/railties-8.1.3/lib</code>.  RubyGems knows that directory contains a file named <code>rails.rb</code>, so it is a candidate to be &ldquo;activated&rdquo;, which is what RubyGems calls it when a gem is added to your <code>$LOAD_PATH</code>.</p>
<p>As long as internal bookkeeping shows that no other versions of <code>railties</code> have already been added to the <code>$LOAD_PATH</code>, we&rsquo;re good! RubyGems adds this specific directory to the <code>$LOAD_PATH</code>, and delegates to the original implementation of <code>require</code>. Require finds the file at <code>~/.gem/ruby/4.0.0/gems/railties-8.1.3/lib/rails.rb</code>, reads it, and evaluates it.</p>
<h3 id="gem-installed-congratulations">Gem installed, congratulations</h3>
<p>With that, we&rsquo;ve done it! We have found, downloaded, unpacked, and installed a gem so that Ruby is able to run a command and load ruby files, without ever touching the <code>gem install</code> command.</p>
<p>If you&rsquo;re interested in contributing to an open source project that works a lot with gems, we would love to work with you on <code>rv</code>, where we are working to create the fastest Ruby and gem manager in the world.</p>
<p>And of course, if your company could use faster, easier, or more secure gems for developers, for CI, and for production deployments, we can help. We&rsquo;d love to talk to you and you can find our contact information at <a href="https://spinel.coop">spinel.coop</a>.</p>
]]></content></entry><entry><title type="html">Four months of Ruby Central moving Ruby backward</title><link href="https://andre.arko.net/2026/03/03/four-months-of-ruby-central-moving-ruby-backward/"/><updated>2026-03-03T01:50:25-08:00</updated><id>http://andre.arko.net/2026/03/03/four-months-of-ruby-central-moving-ruby-backward/</id><content type="html"><![CDATA[<p>From the moment RubyGems was first created in 2004, <strong>Ruby Central provided <em>governance</em> without claiming <em>ownership</em></strong>, to support the Ruby community. Providing governance meant creating processes to provide stability and predictability. Avoiding ownership meant allowing the community to contribute, to the point where unpaid volunteers created and controlled the entirety of RubyGems.org for many years.</p>
<p><strong>Last year, Ruby Central flipped that successful formula on its head</strong>. They now claim <em>ownership</em> of both Bundler and RubyGems, but refuse to provide <em>governance</em>. Ruby Central now claims sole control over all code and decisions, despite paying for only a few percent of the work required to create and sustain the projects across 22 years. Instead of providing stable and predictable processes, Ruby Central <a href="https://joel.drapper.me/p/rubygems-takeover/">suddenly hijacked the Bundler and RubyGems codebases</a> away from the existing maintainers, shut out the community, and started issuing the threats to sue.</p>
<p>When confronted by the former maintainers after the hijacking, Marty Haught of Ruby Central stated (in <a href="https://gofile.me/6g3JG/oTY79Vk3b">a recorded video call</a>) on September 17 that &ldquo;yeah, we shouldn’t have changed that&rdquo;. On September 18, Marty went on to write:</p>
<blockquote>
<p>In the past, we&rsquo;ve made the mistake of conflating ownership of the code with ownership of the infra, and vice versa, and we&rsquo;d like to straighten this out so that we aren&rsquo;t put in a legal bind that requires us to take control of the entire codebase when, we all agree, that is not proper or correct given the existing model.</p></blockquote>
<p>In the words of Ruby Central itself, <strong>&ldquo;we all agree, [taking control of the entire codebase] is not proper or correct.&rdquo;</strong> Since the beginning of this conflict, Ruby Central has privately admitted it was wrong to hijack the GitHub organization and steal the repos, but has refused to acknowledge this in public. Unfortunately, despite privately admitting their actions were wrong, Ruby Central has publicly continued to dig their hole deeper. Instead of owning up to their mistake, they secretly negotiated a deal with Matz for ruby-core to <a href="https://rubycentral.org/news/ruby-central-statement-on-rubygems-bundler/">take over the stolen RubyGems and Bundler repository</a>, further violating the project governance policies.</p>
<p>If this situation were just about me personally, I could believe it sprang from from individual disagreements. Ruby Central <a href="https://rubycentral.org/news/rubygems-org-aws-root-access-event-september-2025/" title="Incident Response Timeline">claims they had good reasons</a> to unilaterally kick me out of the project, even though <a href="https://andre.arko.net/2025/10/09/the-rubygems-security-incident/">I don&rsquo;t think their claims hold water</a>. With that said, regardless of what you think about me personally, the other <strong>five long-term maintainers have never gotten any explanation</strong> of why they were suddenly kicked out or bypassed entirely, all in violation of existing project governance.</p>
<p>In <a href="https://youtu.be/nKpo68g9dEk?si=317CcZWMDIJNYZQ3&amp;t=949">her only public interview</a> about the situation, Ruby Central Executive Director Shan Cureton defended stealing Bundler from its team of fifteen years by saying the removed team &ldquo;didn&rsquo;t need to have the story, and it wasn&rsquo;t their story to have&rdquo;. Ruby Central has made their position clear: <strong>if they steal your project, you are not entitled to know their reasons</strong>, and neither is anyone else. There is nothing &ldquo;community-oriented&rdquo; about stealing the most-used gem in Ruby and refusing to share your reasons with the community.</p>
<p>Despite Ruby Central’s unacceptable treatment of both projects and maintainers, the former RubyGems and Bundler team said <a href="https://andre.arko.net/2025/10/26/we-want-to-move-ruby-forward/">we want to move Ruby forward</a>. <strong>We offered Ruby Central a path</strong> to move past their illegitimate GitHub takeover, past their vicious personal attacks, and past their threats to sue us.</p>
<p>It has been four months since we made that offer, and <strong>Ruby Central has not accepted</strong>.</p>
<p>While declining to accept our offer, Ruby Central has nonetheless found the time to <a href="https://github.com/ruby/rubygems/pull/9187">propose new governance documents for RubyGems</a>. In those documents, they explicitly require existing maintainers approve adding or removing team members. That rule was already present in the previous governance, and is <strong>the exact rule that Ruby Central violated to execute their takeover</strong>. When asked why they violated the previous governance, and why the new governance would be any more trustworthy, Ruby Central refused to respond substantively, and then the question itself was hidden by <a href="https://github.com/ruby/rubygems/pull/9187#issuecomment-3703919204">marking it &ldquo;off topic&rdquo;</a>.</p>
<p>Instead of working to resolve the situation, Ruby Central has spent 4 months rejecting requests for an explanation, while repeatedly threatening to sue me personally. After Ruby Central suddenly took over the Bundler repo, <a href="https://andre.arko.net/2025/09/25/bundler-belongs-to-the-ruby-community/">I sent them a standard trademark notice</a>. They replied with a threat to sue me. When I later informed Ruby Central I had learned they violated state employment law, they simply replied with the same threat to sue me again. They are threatening to sue me for &ldquo;hacking&rdquo; them, despite their own analysis publicly concluding <a href="https://rubycentral.org/news/rubygems-org-aws-root-access-event-september-2025/">&ldquo;no evidence that user data or production operations were harmed&rdquo;</a>.</p>
<p>Without seeking common ground, or even looking for some sort of resolution we can just live with and move on from, <strong>Ruby Central has offered all of us — nothing</strong>. Ruby Central has made no offer in reply to outreach from the other five maintainers. To me, after four grueling months of private &ldquo;negotiation&rdquo;, their entire offer is nothing more than to refrain from suing. But only if I agree to everything that they want.</p>
<p>They say I must agree that I have no claim on the name Bundler, despite helping create it and leading the Bundler team for the last 15 years. They say I must agree I was paid legally and fairly, when California law clearly states I was not. <strong>They say I must agree that Ruby Central can take over open source projects</strong> they host, any time they feel like it, with no explanation, and no consequences.</p>
<p>I don&rsquo;t agree.</p>
<p>Letting this situation stay unaddressed sets a dangerous precedent for all open source projects written in Ruby. <strong>Ruby Central has resolved nothing. Don&rsquo;t let their delaying tactics convince you otherwise.</strong> The Ruby community cannot trust Ruby Central with control over our gems until there is accountability for destroying <a href="https://nesbitt.io/2025/12/22/package-registries-are-governance-as-a-service.html">the very governance they were supposed to be providing</a>.</p>
<p><strong>Until accountability arrives, take action</strong>. Tell Ruby Central they owe everyone an explanation for violating the project governance around six long-term maintainers, not just me. Don&rsquo;t sponsor, attend, or speak at RubyConf. Contribute to projects that aren&rsquo;t controlled by Ruby Central.</p>
<p>The exiled maintainers are working on new projects, with a focus on clear governance, long-term financial sustainability, and community input: Join the <a href="https://gem.coop/updates/5/">gem.coop</a> beta, and stop using RubyGems.org. Use <a href="https://github.com/duckinator/jwl">jwl</a> instead of RubyGems. Use <a href="https://rv.dev"><code>rv</code></a> or <a href="https://github.com/RubyElders/ruby-butler/">Ruby Butler</a> instead of Bundler.</p>
<p>A better world is possible! Ruby Central might want to keep Ruby in the past, but <strong>we can work together to build Ruby a future</strong>.</p>
]]></content></entry><entry><title type="html">Announcing &lt;code>rv clean-install&lt;/code></title><link href="https://andre.arko.net/2026/01/07/rv-clean-install/"/><updated>2026-01-07T12:31:16+08:00</updated><id>http://andre.arko.net/2026/01/07/rv-clean-install/</id><content type="html"><![CDATA[<p><small>Originally posted <a href="https://spinel.coop/blog/rv-clean-install/">on the Spinel blog</a>.</small></p>
<p>As part of our <a href="https://andre.arko.net/2025/08/25/rv-a-new-kind-of-ruby-management-tool/">quest to build a fast Ruby project tool</a>, we&rsquo;ve been hard at work on the next step of project management: installing gems. As we&rsquo;ve learned over the last 15 years of working on Bundler and RubyGems, package managers are really complicated! It&rsquo;s too much to try to copy all of rbenv, and ruby-build, and RubyGems, and Bundler, all at the same time.</p>
<p>Since we can&rsquo;t ship everything at once, we spent some time discussing the first project management feature we should add after Ruby versions. Inspired by <code>npm</code> and <code>orogene</code>, we decided to build <code>clean-install</code>. Today, we&rsquo;re releasing the <code>rv clean-install</code> command as part of <a href="https://github.com/spinel-coop/rv/releases/tag/v0.4.0"><code>rv</code> version 0.4.</a></p>
<p>So, what is a clean install? In this case, clean means &ldquo;from a clean slate&rdquo;. You can use <code>rv ci</code> to install the packages your project needs after a fresh checkout, or before running your tests in CI. It&rsquo;s useful by itself, and it&rsquo;s also concrete step towards managing a project and its dependencies.</p>
<p>Even better, it lays a lot of the groundwork for future gem management functionality, including downloading, caching, and unpacking gems, compiling native gem extensions, and providing libraries that can be loaded by Bundler at runtime.</p>
<p>While we don&rsquo;t (yet!) handle adding, removing, or updating gem versions, we&rsquo;re extremely proud of the progress that we&rsquo;ve made, and we&rsquo;re looking forward to improving <code>rv</code> based on your feedback.</p>
<p>Try running <code>brew install rv; rv clean-install</code> today, and see how it goes. Is it fast? Slow? Are there errors? What do you want to see next? <a href="https://github.com/spinel-coop/rv/issues/new">Let us know what you think</a>.</p>
]]></content></entry><entry><title type="html">Why are &lt;code>exec&lt;/code> and &lt;code>run&lt;/code> so confusing?</title><link href="https://andre.arko.net/2025/12/14/exec-vs-run/"/><updated>2025-12-14T21:20:10-08:00</updated><id>http://andre.arko.net/2025/12/14/exec-vs-run/</id><content type="html"><![CDATA[<p><small>Originally posted <a href="https://spinel.coop/news/exec-vs-run/">on the Spinel blog</a>.</small></p>
<p>While working on <a href="https://rv.dev">rv</a>, there&rsquo;s a specific question that has come up over and over again, in many different forms. In the simplest possible form, it boils down to:</p>
<blockquote>
<p>What is the difference between <code>rv exec</code> and <code>rv run</code>? Why have both?</p></blockquote>
<p>We haven&rsquo;t finished implementing either <code>rv exec</code> or <code>rv run</code> yet, but every time one or the other comes up in a conversation, everything instantly becomes more confusing.</p>
<p>This post will summarize the history of <code>exec</code> and <code>run</code> in Bundler, npm, Cargo, and uv. Once we have the history laid out, we can take a look at what we plan to do in rv, and you can <a href="https://github.com/spinel-coop/rv/discussions/235">give us your feedback</a>.</p>
<h2 id="bundler-exec">Bundler <code>exec</code></h2>
<p>Bundler manages project-specific packages, but not generally available &ldquo;global&rdquo; commands. Project-specific packages installed with Bundler can include their own commands.</p>
<p>While working on Bundler 1.0, we needed a standard way to do something new: run commands completely scoped inside a project, rather than scoped to the entire Ruby installation on the current machine. We tried both a wrapper command (<code>bundle exec COMMAND</code>) and generating dedicated scripts in the project&rsquo;s <code>bin/</code> directory. With binstubs, you could run <code>bin/rake</code> to get the project rake, and <code>rake</code> to get the global rake.</p>
<p>I personally preferred the binstub approach, but it was <code>bundle exec</code> that ultimately became the popular way to use commands inside your project. My theory is that it won because you can use it to run any command, including <code>ruby</code>, or <code>bash</code>, or anything else you want.</p>
<h2 id="rubygems-exec">RubyGems <code>exec</code></h2>
<p>Somewhat confusingly (inspired by the <code>npm exec</code> command explained below) there is a separate <code>gem exec</code> command that is not related to Bundler and instead installs and runs a command from a package. RubyGems only manages global packages and commands, so <code>gem exec</code> is more of a convenience to make it easier to globally install and run a package with just one command.</p>
<h2 id="npm-run-and-exec">npm <code>run</code> and <code>exec</code></h2>
<p>npm manages both project-specific and global packages, and can install any package so its commands are available either only within a project or globally across every project that uses the same version of Node.</p>
<p>The project-focused design of npm expects commands from project packages to be run by first adding the command to <code>package.json</code> in the <code>script</code> section, and the run via <code>npm run SCRIPT</code>. This is even more inconvenient than Bundler&rsquo;s binstubs, and so I think there was pent-up demand to be able to &ldquo;just run a command directly&rdquo;. That was eventually provided by <code>npm exec</code> and its alias <code>npx</code>.</p>
<p>The <code>npx COMMAND</code> setup makes it very easy to run any command, whether the package is in the local project or not, whether a script is set up or not, and even whether the package is installed at all or not. Simply running the command is enough to install the needed package and run its command.</p>
<p>It&rsquo;s especially helpful to have <code>npx</code> available when you need to create a new project by running an npm package, since it&rsquo;s a huge pain to create a project and install a package and set up a script just so you can run the package to overwrite your project. The most popular example of this I am aware of is <code>npx create-react-app</code>, but it&rsquo;s a popular idiom for many packages that contain commands.</p>
<h2 id="cargo-run-and-install">Cargo <code>run</code> and <code>install</code></h2>
<p>Cargo is simalarly a package manager, but unlike Ruby and Node, project-level packages do not include commands, and package commands are installed globally. Library packages are added to a project with <code>cargo add</code>, while command packages are installed globally with <code>cargo install</code>. Once a package is installed globally, it can simply be available on your <code>$PATH</code>, and Cargo no longer has to be involved in running it.</p>
<p>The <code>cargo run</code> command is extremely limited in scope, and only allows you to build and run a binary created by your own project &ndash; it does not work to run commands from packages, either project-local or global.</p>
<h2 id="uv-exec-and-run">uv <code>exec</code> and <code>run</code></h2>
<p>In uv, the <code>exec</code> command seems to be most strongly inspired by <code>npm exec</code>, including having its own short alias of <code>uvx</code>. The <code>uv exec</code> command is exclusively for running commands directly from packages, automatically installing them if necessary. To give an example, that means you can use <code>uv exec github-backup</code> to install and run the github-backup command from the Python package named github-backup, whether or not that packge is included in your current project.</p>
<p>Conversely, the <code>uv run</code> command is closer to <code>bundle exec</code>: it installs and configures Python, installs project packages if inside a project, and then runs a command from <code>$PATH</code> or runs a file. That means <code>uv run</code> can be used for both <code>uv run bash</code> to get a shell with only your project&rsquo;s Python and packages, and can also be used as <code>uv run myscript.py</code> to run a script file directly.</p>
<h2 id="summary">Summary</h2>
<p><code>bundle exec</code> runs:</p>
<ul>
<li>commands created by your package</li>
<li>commands from project packages (like <code>bundle exec rails</code>)</li>
<li>commands from $PATH (like <code>bundle exec bash</code>)</li>
<li>scripts from files (like <code>bundle exec ./myscript.rb</code>)</li>
</ul>
<p><code>npm run</code> runs:</p>
<ul>
<li>project-defined script commands (like <code>npm run my-project-script</code>), which can call:
<ul>
<li>commands from project packages (like <code>generate_pdf.js</code>)</li>
<li>commands from $PATH (like <code>bash</code>)</li>
<li>scripts from files (like <code>./myscript.js</code>)</li>
</ul>
</li>
</ul>
<p><code>npm exec</code> installs and runs:</p>
<ul>
<li>non-project commands from any package (like <code>npx create-react app</code>)</li>
</ul>
<p><code>cargo run</code> builds and runs:</p>
<ul>
<li>commands created by your package</li>
</ul>
<p><code>uv run</code> installs python and any project packages, then runs:</p>
<ul>
<li>commands from project packages (like <code>uv run datasette</code>)</li>
<li>commands from $PATH (like <code>uv run python</code>)</li>
<li>scripts from files (like <code>uv run ./myscript.py</code>)</li>
<li>project-defined script commands (like <code>uv run my-project-script</code>), which can call:
<ul>
<li>commands from project packages (like <code>github-backup</code>)</li>
<li>commands from $PATH (like <code>bash</code>)</li>
<li>scripts from files (like <code>./myscript.py</code>)</li>
</ul>
</li>
</ul>
<p><code>uv exec</code> installs python and the named package, then runs:</p>
<ul>
<li>non-project commands from any package (like <code>uv exec github-backup</code>)</li>
</ul>
<h2 id="the-question-for-rv">The question for <code>rv</code></h2>
<p>With all of that background now set up, what should <code>rv exec</code> do? What should <code>rv run</code> do?</p>
<p>To be the most like Bundler, we should use <code>rv exec</code> to run commands for a project. To be the most like <code>npm</code> and <code>uv</code>, we should use <code>rv exec</code> to install and run a single package with a command.</p>
<p>Today, we&rsquo;re leaning towards an option that includes all of the useful functionality from every command above, and aligns Ruby tooling with JS tooling and Python tooling:</p>
<p><code>rv run</code> installs ruby and any project packages, then runs:</p>
<ul>
<li>commands from project packages (like <code>rv run rspec</code>)</li>
<li>commands from $PATH (like <code>rv run bash</code>)</li>
<li>scripts from files (like <code>rv run ./myscript.rb</code>)</li>
<li>project-defined script commands (like <code>rv run my-project-script</code>), which can call:
<ul>
<li>commands from project packages (like <code>rspec</code>)</li>
<li>commands from $PATH (like <code>bash</code>)</li>
<li>scripts from files (like <code>./myscript.rb</code>)</li>
</ul>
</li>
</ul>
<p><code>rv exec</code> installs ruby and the named package, then runs:</p>
<ul>
<li>non-project commands from any package (like <code>rv exec rails</code>)</li>
</ul>
<p>If we try to combine those two commands into one command, we quickly run into the ambiguity that has been so frustrating to handle in Bundler for all of these years: if you <code>rv run rake</code>, do you want the global rake package? Or do you want the project-local rake package? How can we know which you want?</p>
<p>In my opinion, <code>uv</code> solves this relatively elegantly by having <code>uvx</code> always run a package globally, and <code>uvr</code> always run a package locally inside the current project, if one exists.</p>
<p>What do you think? <a href="https://github.com/spinel-coop/rv/discussions/235">Let us know in the GitHub discussion about this post.</a></p>
]]></content></entry><entry><title type="html">Operating Rails: what about after you deploy?</title><link href="https://andre.arko.net/2025/11/20/operating-rails/"/><updated>2025-11-20T10:52:56+09:00</updated><id>http://andre.arko.net/2025/11/20/operating-rails/</id><content type="html"><![CDATA[<p><small>This post was originally given as a talk at <a href="https://rockymtnruby.dev">Rocky Mountain Ruby</a>. The <a href="https://speakerdeck.com/indirect/operating-rails-what-about-after-you-deploy">slides</a> and <a href="https://www.youtube.com/watch?v=WP2fWUBPGfI">video</a> are also available.</small></p>
<script defer class="speakerdeck-embed" data-id="64254bb94df74086b8ece33274a25524" data-ratio="1.7777777777777777" src="//speakerdeck.com/assets/embed.js"></script>
<p>Welcome! This is meant to serve as an introduction to deployment and operations for newer developers, but it&rsquo;s also a checklist that I refer back to even after 20 years of deploying Rails apps to production. What needs to be covered when going to production the first time? What needs to be covered when going to production for the 1000th time?</p>
<p>Before we get into those details, let me introduce myself. I&rsquo;m André Arko, better known as @indirect on the internet. My biggest Ruby claim to fame is leading the Bundler and RubyGems OSS team for the last 15 years or so. Ruby Central recently kicked out the existing team, so the former RubyGems team has set up a new community-focused gem server at <a href="https://gem.coop">https://gem.coop</a>. I hope you&rsquo;ll check it out.</p>
<p>But I&rsquo;m here to talk about deploying and operating Rails applications, so let&rsquo;s get going. I built my first Rails application in 2004, and trying to get that application off my laptop so other people could use it is what started me down the path to madness and devops. It&rsquo;s been a long journey, and I have learned many things from over 20 years of watching Rails apps break in production.</p>
<p>Let&rsquo;s set the scene: an excited Ruby developer follows a Rails tutorial, creating a blog, a todo app, or some other cool idea. They walk through the code, the tests, the features. They finish the tutorial, and have a full Rails application, something cool that they want to show off to other people! That&rsquo;s when they realize… this is only on their laptop, and no one else can see or use it.</p>
<p>The most useful tutorials will mention hosting options at this point, maybe Heroku, Fly.io, Render, Railway, or DigitalOcean. If they&rsquo;re unhelpful tutorials, they might mention AWS, GCP, or Azure. But no matter what service they suggest, coding tutorials need to stay focused, and refer developers out to a hosting service when they want to deploy.</p>
<p>This is a problem, because the tutorials about how to deploy on those hosting sites all say things like &ldquo;of course this introduction does not include how to secure your secrets in production&rdquo;. In fact, hosting tutorials usually just wave their hands and skip over most aspects of deploying and operating a true production application. I&rsquo;m not sure how they expect a new dev to learn these skills, exactly, but that&rsquo;s what I&rsquo;m going to try to cover in this talk today.</p>
<p>So our hypothetical Rails developer has followed a hosting tutorial, and has a shiny new application and database deployed to Heroku, or Fly.io, or Render, or whatever. Now is when they actually start to run up against the real problems of running in production: are the secrets secure? is the database backed up? what happens if there&rsquo;s an exception? how do you debug something that only happens on the server?</p>
<p>Let&rsquo;s break down each of the new kinds of preparation needed by category, and walk through what you need to do, and why you need to do it. Ultimately, everything that we are going to talk about today is about <em>confidence</em>. Everything happening in deployment and operations, in devops, in SRE, is all being done to increase confidence in the application—your own, your coworkers, or your customers.</p>
<p>The areas we&rsquo;re going to look at today are Errors, Data, Speed, Security, and what usually gets called Lead Time. Lead Time is the amount of time it takes for a change to go from a commit to live in production. Investing in a shorter Lead Time can return absolutely outsized benefits in every other area, because it means you can try a change, learn from it, and make another change in reaction, faster and faster. We&rsquo;ll talk a bit more about it at the end.</p>
<h3 id="errors">Errors</h3>
<p>To start with, let&rsquo;s focus on Errors. Conceptually, an error is any time that your user isn&rsquo;t able to use your site. More specifically, an error could mean your Ruby code threw an exception, it could mean some configuration is preventing your app from working correctly, or it could mean that your servers have completely blown up and there is no app available for your users to talk to. With that in mind… how will you know? That&rsquo;s the first and most important rule of running applications in production. You need to know in advance how you&rsquo;re going to find out if something is broken.</p>
<p>It&rsquo;s not popular deployment advice, but the most important way to build confidence and in your application is to test it. Test it manually, test it automatically, have other people try it and report back. Each of those things will save you more time debugging than you can even imagine right now.</p>
<p>Next, one of the most popular ways to increase confidence before a deploy is a second environment where you can deploy your code to test it before it is live on the main site. The test area is usually called &ldquo;staging&rdquo;, and the main area is usually called &ldquo;production&rdquo;. Rails has built-in support for both of those, as well as &ldquo;development&rdquo;, which runs by default on your local development machine.</p>
<p>Use the staging environment as a place where you can put code that you aren&rsquo;t sure about, and try the new version there. After you have confidence (there it is again) in your new changes, you can &ldquo;promote&rdquo; them to production. Some platforms even let you bump staging to production without having to wait for a full new production deploy.</p>
<p>After you&rsquo;re releasing code with manual and automatic tests to staging and then production, the next most common mitigation strategies are exception reporting and uptime monitoring. Exception reporting does exactly what it sounds like, and sends a report to you when there is an exception in your application.</p>
<p>Personally, I tend to use honeybadger.io to set up exception tracking and uptime monitoring. It&rsquo;s free for a single user, which is great for my personal project, and I am apple to connect it to Slack so I get notifications if there are Ruby or Javascript errors, or if the site stops being reachable from the internet. Other popular options for tracking exceptions include Sentry, Airbrake, Bugsnag, and more that you can easily find with a search.</p>
<p>For anything that&rsquo;s less significant than an exception, or to help you investigate when an exception does occur, you&rsquo;ll also want to think about what usually gets called &ldquo;instrumentation&rdquo; or &ldquo;observability&rdquo;. The most basic level here is just seeing the logs from your application, and most hosting will provide a way for you to do that.</p>
<p>The more advanced levels of observability include setting up additional tracking in your application, both inside the Rails framework and inside your own code. This tracking can take the form of metrics, which are numbers counting when and how many times some particular thing has happened. Monitoring that focuses primarily on metrics includes using the Prometheus open source tool, or DataDog, AppSignal, NewRelic, and others like them.</p>
<p>Monitoring can also take the form of traces, where your application keeps track of how long each part of a response takes, letting you see each controller, parameter, database query, http request, and anything else that happened. If you&rsquo;ve ever heard of OpenTelemetry, this is what that is about. Honeycomb.io is the pioneer of this style of monitoring, but others like Sentry and DataDog have also added the ability to collect and review traces.</p>
<p>I don&rsquo;t always find Honeycomb&rsquo;s &ldquo;only events and traces&rdquo; style easier to use than simply reading logs, but it regularly helps me solve debugging mysteries that I would not have been able to figure out from solely reading logs, so I&rsquo;m very glad to have it as an option.</p>
<p>Lastly on the topic of errors in production, the open-ended question to ask yourself is &ldquo;how will people tell me about problems?&rdquo;. Do you have a contact form? A support email? A social media profile? Make sure there&rsquo;s some way to hear from your users, because there will always be something that doesn&rsquo;t set off your monitoring but still needs to be fixed… if you can hear about it.</p>
<h3 id="data">Data</h3>
<p>Next up is Data. Data includes not just the stuff that users have uploaded and put directly into your database, it also includes uploaded files, and anything else that you might want while recovering from a catastrophic server failure. Server failures range from the very mundane server part died to the very surprising truck rammed into the data center&rsquo;s power transformer, but they all have the same end state: your server is gone.</p>
<p>Now that your server is gone, you need to set up something again. This is the touchiest part of any production service. Do you have a copy of the latest code? A recent database backup? Uploaded files saved somewhere you can still get to them?</p>
<p>Start with the code. Make sure that you deploy to production by pushing to a code host and deploying from there. Most often that means GitHub and a deploy from GitHub Actions, but the important part is that any code that goes live needs to be easy to find even if your laptop vanishes.</p>
<p>Next, handle the database. Use a system that automatically takes daily snapshots of your database, or build something yourself that creates daily copies. For my side projects, I have set up a daily GitHub Action that connects to my database, pulls out a copy, and uploads it as an artifact. Actions artifacts have no size limit, and just time out after 90 days, which is perfect for daily backups.</p>
<p>Test your backups. More than one startup has completely failed and shut down when they discovered that their backups weren&rsquo;t actually running, or the results were corrupted, only after it was too late. Have more than one backup, preferably one per day, and test your backups system regularly. That&rsquo;s the second, and most important, rule of running applications in production.</p>
<p>Once you have your database(s) taken care of, it&rsquo;s time to look at your user uploaded or created files. Using a file service like S3, B2, R2, Google Cloud Storage, or Tigris can be a huge advantage here. Those services all provide a basic promise that your files will be copied to at least 3 places at all times. While it&rsquo;s possible to run your own copy of Minio and treat it like S3, if the filesystem backing your Minio server crashes, those files are gone. I don&rsquo;t recommend doing that unless you&rsquo;re sure those files don&rsquo;t really matter.</p>
<p>Lastly for data, you now need to think about how you are going to deal with all of the existing production data, now constantly growing, that you can&rsquo;t just erase. In local development, if your schema gets into a weird state, it&rsquo;s easy to just create a new database and import the schema again. Now that you have a live, production database, you can&rsquo;t do that.</p>
<p>Adding, removing, and renaming database columns are all now potentially actions that could break your application. To avoid full site downtime, you need to deploy code that can handle both the old schema and the new schema, then run the schema migration, then remove the handler code and deploy again. I recommend adding the <a href="https://github.com/ankane/strong_migrations"><code>strong_migrations</code></a> gem as soon as you have a live production site, to add some guardrails that will make it harder to accidentally break either the site or your users&rsquo; data.</p>
<h3 id="speed">Speed</h3>
<p>Now let&rsquo;s talk about speed. Speed from a Rails app? It&rsquo;s possible! In my experience, the biggest reasons that Rails apps get slow are external API calls, responses with no pagination, and accidental N+1 database queries. Rails may not be the most performant framework in the world, but it can be perfectly usable as long as you avoid those traps.</p>
<p>Why does the speed of your application matter? As mentioned at the start, it comes down to confidence. If users can see what is happening quickly, they trust your app to be working correctly. If your application matter takes so long that users lose interest or get distracted, that confidence is gone. Whatever did happen with that thing… hmm, I dunno.</p>
<p>Use the monitoring and observability tools we talked about before to keep an eye on how long your application is taking to do things. Don&rsquo;t use the average, because averages don&rsquo;t reflect any actual user experience. Instead, track a percentile that reflects how many users you can tolerate having a bad experience.</p>
<p>If you have 10 users per day, maybe the 90th percentile performance is what to watch because only 1 person will have a worse experience. If you have 10 million users per day, you might want to track the 99.99th percentile, because that&rsquo;s still 1000 people every day having a worse experience than that number.</p>
<p>When you find something that&rsquo;s slow, your main tools to fight against it are monitoring and profiling. Monitoring can tell you which users experienced the slowness, and what they were trying to do. It might even be able to tell you which SQL queries or HTTP calls made things so slow. Meanwhile, profiling can tell you what code is being run inside a given action, and help you optimize it. The <code>rack-prof</code> gem is the most well-known profiler, but there are multiple options available.</p>
<h3 id="security">Security</h3>
<p>Now that you know how and why to keep your app fast, let&rsquo;s talk about security. There are two resources your production application has that you now need to defend: user data, and servers on the internet. One class of attacks will try to break in to your application to steal user data, like emails, phone numbers, whatever you have stored. The other type of attack is about hijacking your server to do something for the attacker, like send spam or show ads or mine cryptocurrency.</p>
<p>The best defense for user data is to not store it at all. If you don&rsquo;t absolutely need it, don&rsquo;t collect it, and then no one can steal it. If you need to collect user information, make sure you are only collecting what you need. If your users are supplying personal information, especially sensitive financial or identity information, make sure the data is encrypted inside the database so a simple database leak won&rsquo;t reveal it. It&rsquo;s not hard to encrypt a single model column anymore, so be sure to do it!</p>
<p>The best defense against attackers who want to hijack your servers is to update your libraries. Use a tool like Dependabot or Renovate to apply security fixes, so your application isn&rsquo;t vulnerable to known hacks. Set up a monitoring alert on your CPU and disk usage. Set up a billing alert for if your usage goes above your usual amount, so you can investigate.</p>
<p>Ultimately, it&rsquo;s always worth it to have a security policy, even if it&rsquo;s very simple. Provide contact information so problems can be brought to your attention. You don&rsquo;t need to have a security team and a HackerOne program, but you do need to have a basic awareness of what could go wrong and how you can approach handling it. Knowing you have backups that can&rsquo;t be deleted by someone hacking into your server is a huge advantage here.</p>
<h3 id="lead-time">Lead Time</h3>
<p>Finally, lead time. Lead time is one of the for metrics tracked by the DORA program&rsquo;s research into software development teams. The book <em>Accelerate</em>, written by the founders of the DORA research program, goes into much more depth than I can fit here, so I&rsquo;m going to briefly summarize just this one aspect.</p>
<p>Lead time is the amount of time it takes to go from code being committed to that same code being live in production. If you can make a change and get it deployed into production in just a few minutes, you will have a completely different experience developing your application than if it takes hours or days. The DORA research found that small lead times is one of the biggest predictors of software projects being successful.</p>
<p>The overall, main finding of the DORA research project is that conventional wisdom of going slowly and carefully is exactly backwards—the teams and projects with the least errors and rollbacks are the teams that deploy the fastest and the most frequently. If you&rsquo;re already operating a larger or slower application in production, the biggest advice that I have is to reduce the size of each change, and make rolling out each change as fast as you possibly can.</p>
<p>The beginning of a project is by far the easiest time to set up the standard automations that you plan to use going forward. I strongly recommend setting up completely automatic testing and deployment. With GitHub Actions and modern hosting platforms, you can expect new commits to be tested and fully deployed to production within 5 minutes. That&rsquo;s a fantastic starting point to use as a base for future expansion, as your application and team grows.</p>
<h3 id="strategy">Strategy</h3>
<p>Now that I&rsquo;ve filled your heads with more ideas than you can probably remember, let&rsquo;s quickly review and then briefly discuss an overall strategy that you can use whenever you are trying to operate a production application. The big concerns after you deploy your app are: Errors, Data, Speed, Security, and Lead Time. Use those categories to review proposed changes, and look for issues you might have missed. Will the proposed change impact your ability to monitor errors? Back up user data? Respond to requests quickly? Keep your application secure? Ship changes quickly?</p>
<p>By keeping those categories of concern in mind, you are already better prepared to operate a production deployment than the vast majority of Rails developers. If the thought of working with those concerns is interesting or exciting to you, you probably have a bright future in DevOps or SRE. If the thought of working with those concerns fills you with unspeakable dread, you probably want to stick to bigger teams where someone else is available to handle operations and deployment.</p>
<h3 id="self-promotion">Self-promotion</h3>
<p>If your team could use a boost while handling everything we&rsquo;ve talked about today, you can get that support. I work for <a href="https://spinel.coop">Spinel Cooperative</a>, with developers from the core teams of Rails, Hotwire, RubyGems, and more. We would love to give your team all the advantages of 20 years spent building, deploying, and operating Ruby and Rails. Come say hi after the talk, or drop us an email at <a href="mailto:hello@spinel.coop">hello@spinel.coop</a>.</p>
<h3 id="conclusion">Conclusion</h3>
<p>In the end, will listening to all the advice in this talk mean your app never goes down? Unfortunately, probably not. Downtime is something that comes for us all, and usually includes some aspect that is completely unexpected or outside of our control. Instead, this advice will make you prepared. You&rsquo;ll be prepared to recover from downtime, and prepared to add more protections in the future as you learn the particular issues your application needs to defend against.</p>
<p>Ultimately, that&rsquo;s the best any of us can do, and I wish you all good luck with your future deployments.</p>
]]></content></entry><entry><title type="html">We want to move Ruby forward</title><link href="https://andre.arko.net/2025/10/26/we-want-to-move-ruby-forward/"/><updated>2025-10-26T12:21:18+09:00</updated><id>http://andre.arko.net/2025/10/26/we-want-to-move-ruby-forward/</id><content type="html"><![CDATA[<p>On September 9, without warning, Ruby Central <a href="https://pup-e.com/blog/goodbye-rubygems/">kicked out the maintainers</a> who have cared for Bundler and RubyGems for over a decade. Ruby Central made these changes against the <a href="https://github.com/ruby/rubygems/blob/master/doc/rubygems/POLICIES.md#committer-access">established project policies</a>, while ignoring all <a href="https://gist.github.com/simi/349d881d16d3d86947945615a47c60ca">objections from the maintainers&rsquo; team</a>. At the time, <a href="https://rubycentral.org/news/strengthening-the-stewardship-of-rubygems-and-bundler/">Ruby Central claimed</a> these changes were “temporary&quot;. However,</p>
<ul>
<li>None of the “temporary” changes made by Ruby Central have been undone, more than six weeks later.</li>
<li>Ruby Central still has not communicated with the removed maintainers about restoring any permissions.</li>
<li>Ruby Central still has not offered “operator agreements” or “contributor agreements” to any of the removed maintainers.</li>
<li>The <a href="https://andre.arko.net/2025/09/25/bundler-belongs-to-the-ruby-community/merger-agreement.pdf">Ruby Together merger agreement</a> plainly states that it is the maintainers who will decide what is best for their projects, not Ruby Central.</li>
<li>Last week, Matz stepped in to assume control of RubyGems and Bundler himself. <a href="https://www.ruby-lang.org/en/news/2025/10/17/rubygems-repository-transition/">His announcement</a> states that the Ruby core team will assume control and responsibility for the primary RubyGems and Bundler GitHub repository.</li>
<li>Ruby Central did not communicate with any removed maintainers before transferring control of the <a href="https://github.com/rubygems/rubygems/">rubygems/rubygems</a> GitHub repo to the Ruby core team.</li>
<li>On October 24th, Shan publicly confirmed <a href="https://youtu.be/nKpo68g9dEk?list=PLdqi4WM39BUiorBaKf4KfhejVDm0Uu0ew&amp;t=823">she does not believe the maintainers need to be told why they were removed</a>.</li>
</ul>
<p>While we know that Ruby Central had no right to act the way they did, it is nevertheless clear to us that the Ruby community will be better off if the codebase, maintenance, and legal rights to RubyGems and Bundler are all together in the same place.</p>
<p>To bring this about, <strong>we are prepared to transfer our interests in RubyGems and Bundler to Matz</strong>, end the dispute over the GitHub enterprise account, 2 GitHub organizations, and 70 repositories, and hand over all rights in the Bundler logo and Bundler name, including the trademark applications in the US, EU, and Japan.</p>
<p>Once we have entered into a legal agreement to settle any legal claims with Ruby Central and transfer all rights to Matz, the former maintainers will step back entirely from the RubyGems and Bundler projects, leaving them fully and completely to Matz, and by extension to the entire Ruby community.</p>
<p>Although Ruby Central’s actions were not legitimate, our commitment to the Ruby community remains strong. We’re choosing to focus our energy on projects to improve Ruby for everyone, including <a href="https://rv.dev/">rv</a>, <a href="https://github.com/RubyElders/ruby-butler">Ruby Butler</a>, <a href="https://github.com/duckinator/jim">jim</a>, and <a href="https://gem.coop/">gem.coop</a>.</p>
<p>Signed,<br>
The former maintainers: <a href="https://github.com/indirect">André</a>, <a href="https://github.com/deivid-rodriguez">David</a>, <a href="https://github.com/duckinator">Ellen</a>, <a href="https://github.com/simi">Josef</a>, <a href="https://github.com/martinemde">Martin</a>, and <a href="https://github.com/segiddins">Samuel</a></p>
]]></content></entry><entry><title type="html">&lt;code>jj&lt;/code> part 4: configuration</title><link href="https://andre.arko.net/2025/10/15/jj-part-4-configuration/"/><updated>2025-10-15T19:52:56-09:00</updated><id>http://andre.arko.net/2025/10/15/jj-part-4-configuration/</id><content type="html"><![CDATA[<p>Previously in this series: <a href="/2025/10/12/jj-part-3-workflows/"><code>jj</code> part 3: workflows</a></p>
<p>Just like git, jj offers tiers of configuration that layer on top of one another. Every setting can be set for a single repo, for the current user, or globally for the entire system. Just like git, jj offers the ability to create aliases, either as shortcuts or by building up existing commands and options into new completely new commands.</p>
<p>Completely unlike git, jj also allows configuring revset aliases and default templates, extending or replacing built-in functionality. Let&rsquo;s look at the ways it&rsquo;s possible to customize jj via configurations. We&rsquo;ll cover basic config, custom revsets, custom templates, and custom command aliases.</p>
<h3 id="config-basics">config basics</h3>
<p>Let&rsquo;s start with some configuration basics. You can globally configure jj to change your name and email based on a path prefix, so you don’t have to remember to set your work email separately in each work repo anymore.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-Toml" data-lang="Toml"><span style="display:flex;"><span>[[<span style="color:#a6e22e">--scope</span>]]
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">--when</span>.<span style="color:#a6e22e">repositories</span> = [<span style="color:#e6db74">&#34;~/work&#34;</span>]
</span></span><span style="display:flex;"><span>[<span style="color:#a6e22e">--scope</span>.<span style="color:#a6e22e">user</span>]
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">email</span> = <span style="color:#e6db74">&#34;me@work.domain&#34;</span>
</span></span></code></pre></div><p>Or perhaps you want jj to wait for your editor if you are writing a commit message, but you don&rsquo;t want jj to wait for your editor to exist if you are editing your jj configuration file. You can ensure that using a scope.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-Toml" data-lang="Toml"><span style="display:flex;"><span><span style="color:#a6e22e">ui</span>.<span style="color:#a6e22e">editor</span> = <span style="color:#e6db74">&#34;code -w&#34;</span>
</span></span><span style="display:flex;"><span>[[<span style="color:#a6e22e">--scope</span>]]
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">--when</span>.<span style="color:#a6e22e">commands</span> = [<span style="color:#e6db74">&#34;config&#34;</span>]
</span></span><span style="display:flex;"><span>[<span style="color:#a6e22e">--scope</span>.<span style="color:#a6e22e">ui</span>]
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">ui</span>.<span style="color:#a6e22e">editor</span> = <span style="color:#e6db74">&#34;code&#34;</span>
</span></span></code></pre></div><p>I also highly recommend trying out multiple options for formatting your diffs, so you can find the one that is most helpful to you. A very popular diff formatter is <code>difftastic</code>, which provides syntax aware diffs for many languages. I personally use <code>delta</code>, and the configuration to format diffs with delta looks like this:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-Toml" data-lang="Toml"><span style="display:flex;"><span>[[<span style="color:#a6e22e">--scope</span>]]
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">--when</span>.<span style="color:#a6e22e">commands</span> = [<span style="color:#e6db74">&#34;diff&#34;</span>, <span style="color:#e6db74">&#34;show&#34;</span>]
</span></span><span style="display:flex;"><span>[<span style="color:#a6e22e">--scope</span>.<span style="color:#a6e22e">ui</span>]
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">pager</span> = <span style="color:#e6db74">&#34;delta&#34;</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">diff-formatter</span> = <span style="color:#e6db74">&#34;:git&#34;</span>
</span></span></code></pre></div><p>Another very impactful configuration is which tool jj uses to handle interactive diff editing, such as in the <code>jj split</code> or <code>jj squash -i</code> commands. While the default terminal UI is pretty good, make sure to also try out Meld, an open source GUI.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-Toml" data-lang="Toml"><span style="display:flex;"><span>[<span style="color:#a6e22e">ui</span>]
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">diff-editor</span> = <span style="color:#e6db74">&#34;meld&#34;</span> <span style="color:#75715e"># or vimdiff, vscode, etc</span>
</span></span></code></pre></div><p>In addition to changing the diff editor, you can also change the merge editor, which is the program that is used to resolve conflicts. Meld can again be a good option, as well as any of several other merging tools.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-Toml" data-lang="Toml"><span style="display:flex;"><span>[<span style="color:#a6e22e">ui</span>]
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">merge-editor</span> = <span style="color:#e6db74">&#34;meld&#34;</span> <span style="color:#75715e"># or vimdiff, vscode, mergiraf etc</span>
</span></span></code></pre></div><p>Tools like <a href="https://mergiraf.org/">mergiraf</a> provide a way to attempt syntax-aware automated conflict resolution before handing off any remaining conflicts to a human to resolve. That approach can dramatically reduce the amount of time you spend manually handling conflicts.</p>
<p>You might even want to try FileMerge, the macOS developer tools built-in merge tool. It supports both interactive diff editing and conflict resolution.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-Toml" data-lang="Toml"><span style="display:flex;"><span>[<span style="color:#a6e22e">merge-tools</span>.<span style="color:#a6e22e">filemerge</span>]
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">program</span> = <span style="color:#e6db74">&#34;open&#34;</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">edit-args</span> = [<span style="color:#e6db74">&#34;-a&#34;</span>, <span style="color:#e6db74">&#34;FileMerge&#34;</span>, <span style="color:#e6db74">&#34;-n&#34;</span>, <span style="color:#e6db74">&#34;-W&#34;</span>, <span style="color:#e6db74">&#34;--args&#34;</span>,
</span></span><span style="display:flex;"><span>             <span style="color:#e6db74">&#34;-left&#34;</span>, <span style="color:#e6db74">&#34;$left&#34;</span>, <span style="color:#e6db74">&#34;-right&#34;</span>, <span style="color:#e6db74">&#34;$right&#34;</span>,
</span></span><span style="display:flex;"><span>             <span style="color:#e6db74">&#34;-merge&#34;</span>, <span style="color:#e6db74">&#34;$output&#34;</span>]
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">merge-args</span> = [<span style="color:#e6db74">&#34;-a&#34;</span>, <span style="color:#e6db74">&#34;FileMerge&#34;</span>, <span style="color:#e6db74">&#34;-n&#34;</span>, <span style="color:#e6db74">&#34;-W&#34;</span>, <span style="color:#e6db74">&#34;--args&#34;</span>,
</span></span><span style="display:flex;"><span>              <span style="color:#e6db74">&#34;-left&#34;</span>, <span style="color:#e6db74">&#34;$left&#34;</span>, <span style="color:#e6db74">&#34;-right&#34;</span>, <span style="color:#e6db74">&#34;$right&#34;</span>,
</span></span><span style="display:flex;"><span>              <span style="color:#e6db74">&#34;-ancestor&#34;</span>, <span style="color:#e6db74">&#34;$base&#34;</span>, <span style="color:#e6db74">&#34;-merge&#34;</span>, <span style="color:#e6db74">&#34;$output&#34;</span>,]
</span></span></code></pre></div><p>Just two more configurations before we move on to templates. First, the default subcommand, which controls what gets run if you just type <code>jj</code> and hit return. The default is to run <code>jj log</code>, but my own personal obsessive twitch is to run <code>jj status</code> constantly, and so I have changed my default subcommand to <code>status</code>, like so:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-Toml" data-lang="Toml"><span style="display:flex;"><span>[<span style="color:#a6e22e">ui</span>]
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">default-command</span> = [<span style="color:#e6db74">&#34;status&#34;</span>]
</span></span></code></pre></div><p>The last significant configuration is the default revset used by <code>jj log</code>. Depending on your work patterns, the multi-page history of commits in your current repo might not be helpful to you. In that case, you can change the default revset shown by the log command to one that’s more helpful. My own default revset shows only one change from my origin. If I want to see more than the newest change from my origin I use <code>jj ll</code> to get the longer log, using the original default revset. I&rsquo;ll show that off later.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-Toml" data-lang="Toml"><span style="display:flex;"><span>[<span style="color:#a6e22e">revsets</span>]
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">log</span> = <span style="color:#e6db74">&#34;(trunk()..@):: | (trunk()..@)-&#34;</span>
</span></span></code></pre></div><h3 id="templates">templates</h3>
<p>While the <a href="https://jj-vcs.github.io/jj/latest/templates/">jj template docs</a> are a great reference, they don’t do very much to show off what’s possible by using templates, so we’ll show some examples.</p>
<p>The first and most obvious template is the <code>jj log</code> template, which controls how each change is rendered in the log output. The default template is named <code>builtin_log_compact</code>, and jj comes with a few pre-built template options for the log view, like <code>builtin_log_detailed</code> and <code>builtin_log_oneline</code>. You can see them all by running <code>jj log -T</code>.</p>
<p>Use <code>jj log -T NAME</code> to try them out and see how they look. If you want to experiment with your own custom log formats, you can provide a template string instead of the name of an existing template. Here’s an example inline template that prints out just the short change ID, a newline, and then the change description:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>jj log -T <span style="color:#e6db74">&#39;change_id.short() ++ &#34;\n&#34; ++ description&#39;</span>
</span></span></code></pre></div><p>Try out the various <a href="https://jj-vcs.github.io/jj/latest/templates/">documented template properties</a> yourself! Once you’re happy with a template that you’ve tested, you can add it to your config with a name, and then use it by name.</p>
<p>Here’s a more complicated example, adapted from @marchyman in the jj Discord, with several of the elements that we’ve discussed so far. This example changes the default command, adding extra options. It also uses a named revset alias, and a named template alias.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#f92672">[</span>ui<span style="color:#f92672">]</span>
</span></span><span style="display:flex;"><span>default-command <span style="color:#f92672">=</span> <span style="color:#f92672">[</span><span style="color:#e6db74">&#34;log&#34;</span>, <span style="color:#e6db74">&#34;-T&#34;</span>, <span style="color:#e6db74">&#34;log_with_files&#34;</span>, <span style="color:#e6db74">&#34;--limit&#34;</span> <span style="color:#e6db74">&#34;7&#34;</span><span style="color:#f92672">]</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">[</span>revset-aliases<span style="color:#f92672">]</span>
</span></span><span style="display:flex;"><span><span style="color:#e6db74">&#39;recent_work&#39;</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">&#39;ancestors(visible_heads(), 3) &amp; mutable()&#39;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">[</span>template-aliases<span style="color:#f92672">]</span>
</span></span><span style="display:flex;"><span>log_with_files <span style="color:#f92672">=</span> <span style="color:#e6db74">&#39;&#39;&#39;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">if(root,
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">  format_root_commit(self),
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">  label(if(current_working_copy, &#34;working_copy&#34;),
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">    concat(
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">      format_short_commit_header(self) ++ &#34;\n&#34;,
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">      separate(&#34; &#34;,
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">        if(empty, label(&#34;empty&#34;, &#34;(empty)&#34;)),
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">        if(description,
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">          description.first_line(),
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">          label(if(empty, &#34;empty&#34;), description_placeholder),
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">        ),
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">      ) ++ &#34;\n&#34;,
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">      if(self.contained_in(&#34;recent_work&#34;), diff.summary()),
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">    ),
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">  )
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">)
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">&#39;</span>
</span></span></code></pre></div><p>The named template recreates the regular template from <code>log</code>, and uses a revset filter to include the list of changed files in changes that are both mutable (that is, not yet pushed) and also within 3 commits of the end of a branch. By showing up to 7 changes, but the file list for up to 3 mutable changes, the log output becomes more useful, reminding you what files have been changed in the most recent commits that you might want to push.</p>
<p>One more template you might want to adjust is the default description, shown when running <code>jj commit</code> or <code>jj desc</code> for a change that does not yet have a description. If you don’t use a VCS GUI, it can be helpful to see the diff of what is being committed at the same time as you write the commit message. In git, that meant running <code>git commit --verbose</code>, but in jj that means adjusting the default description. The <a href="https://jj-vcs.github.io/jj/latest/config/#default-description">jj config docs</a> provide an example template that will replicate that effect, and show you the diff while you write the message.</p>
<h3 id="revset-aliases">revset aliases</h3>
<p>Building on the earlier section where we talked about <a href="#"><code>jj log</code></a>, creating your own revset aliases is a powerful way to construct views tailored to your personal needs.</p>
<p>A built-in revset alias we can use to illustrate this is <code>immutable()</code>. In the same way that git requires <code>--force</code> to push over an existing remote commit, jj requires <code>--ignore-immutable</code> to edit a commit matched by <code>immutable()</code>.</p>
<p>(Incidentally, I believe this arrangement is also an example of the way jj’s design is an improvement on git. Instead of deciding to overwrite published commits during a push, you are forced to decide much earlier, during the edit itself, if you are okay with changing a published commit. Anyway, back to revset aliases.)</p>
<p>The default <code>immutable_heads()</code> revset is <code>present(trunk()) | tags() | untracked_remote_bookmarks()</code>, which composes four other revsets together. Let’s look at each one. The <code>trunk()</code> revset is simply the primary branch, whether it is named <code>main</code> or <code>master</code>, wrapped in <code>present()</code> to remove it if none of those branches exist. The <code>tags()</code> revset is every change that has been given a tag. The <code>untracked_remote_bookmarks()</code> revset is exactly what it sounds like: any branch provided by the remote that you have not manually opted in to tracking locally (which is what you would do if you are working on the branch). All three revsets are combined into one overall list with the <code>|</code> operator. Those heads are then used to construct the full list of immutable commits, which is every ancestor of those heads.</p>
<p>Now that you know how that works, we can change it. For example, perhaps you want the same immutability rules that <code>git</code> provides, where commits are immutable once they have been pushed to any remote at all. In that case, you could add this to your config file:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-Toml" data-lang="Toml"><span style="display:flex;"><span>[<span style="color:#a6e22e">revset-aliases</span>]
</span></span><span style="display:flex;"><span><span style="color:#e6db74">&#34;immutable_heads()&#34;</span> = <span style="color:#e6db74">&#34;present(trunk()) | tags() | remote_bookmarks()&#34;</span>
</span></span></code></pre></div><p>With that configuration, jj will extrapolate that it cannot change any commits on the primary branch, all the commits leading up to a tag, and all commits leading up to a named branch in the remote. If you use this revset, jj will stop you from changing commits once you have pushed them to a branch, since you told it to make those immutable.</p>
<p>With this power at your disposal, you can change the default revset shown when you run <code>jj log</code>, or you can create your own named revsets for your own purposes. You can see <a href="https://github.com/indirect/dotfiles/blob/main/private_dot_config/private_jj/config.toml#L107">my revset aliases</a> in my dotfiles, and read more about the default aliases in the jj docs.</p>
<h3 id="command-aliases">command aliases</h3>
<p>Okay. Now we can talk about command aliases. First up, the venerable <code>tug</code>. In the simplest possible form, it takes the closest bookmark, and moves that bookmark to <code>@-</code>, the parent of the current commit.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-Toml" data-lang="Toml"><span style="display:flex;"><span><span style="color:#a6e22e">tug</span> = [<span style="color:#e6db74">&#34;bookmark&#34;</span>, <span style="color:#e6db74">&#34;move&#34;</span>, <span style="color:#e6db74">&#34;--from&#34;</span>, <span style="color:#e6db74">&#34;heads(::@- &amp; bookmarks())&#34;</span>, <span style="color:#e6db74">&#34;--to&#34;</span>, <span style="color:#e6db74">&#34;@-&#34;</span>]
</span></span></code></pre></div><p>What if you want it to be smarter, though? It could find the closest bookmark, and then move it to the closest <em>pushable</em> commit, whether that commit was <code>@</code>, or <code>@-</code>, or <code>@---</code>. For that, you can create a revset for <code>closest_pushable</code>, and then tug from the closest bookmark to the closest pushable, like this:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-Toml" data-lang="Toml"><span style="display:flex;"><span>[<span style="color:#a6e22e">revset-aliases</span>]
</span></span><span style="display:flex;"><span><span style="color:#e6db74">&#39;closest_pushable(to)&#39;</span> = <span style="color:#e6db74">&#39;heads(::to &amp; mutable() &amp; ~description(exact:&#34;&#34;) &amp; (~empty() | merges()))&#39;</span>
</span></span><span style="display:flex;"><span>[<span style="color:#a6e22e">aliases</span>]
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">tug</span> = <span style="color:#e6db74">&#39;bookmark move --from &#34;heads(::@ &amp; bookmarks())&#34; --to &#34;closest_pushable(@)&#34;&#39;</span>
</span></span></code></pre></div><p>Now your bookmark jumps up to the change that you can actually push, by excluding immutable, empty, or descriptionless commits.</p>
<p>What if you wanted to allow tug to take arguments, for those times when two bookmarks are on the same change, or when you actually want to tug a different bookmark than the closest one? That’s also pretty easy, by adding a second variant of the tug command that takes an argument:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-Toml" data-lang="Toml"><span style="display:flex;"><span><span style="color:#a6e22e">tug</span> = [<span style="color:#e6db74">&#34;util&#34;</span>, <span style="color:#e6db74">&#34;exec&#34;</span>, <span style="color:#e6db74">&#34;--&#34;</span>, <span style="color:#e6db74">&#34;sh&#34;</span>, <span style="color:#e6db74">&#34;-c&#34;</span>, <span style="color:#e6db74">&#34;&#34;&#34;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">if [ &#34;</span><span style="color:#a6e22e">x</span><span style="color:#960050;background-color:#1e0010">$</span><span style="color:#ae81ff">1</span><span style="color:#e6db74">&#34; = &#34;</span><span style="color:#a6e22e">x</span><span style="color:#e6db74">&#34; ]; then
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">  jj bookmark move --from &#34;</span><span style="color:#a6e22e">closest_bookmark</span><span style="color:#960050;background-color:#1e0010">(@)</span><span style="color:#e6db74">&#34; --to &#34;</span><span style="color:#a6e22e">closest_pushable</span><span style="color:#960050;background-color:#1e0010">(@)</span><span style="color:#e6db74">&#34;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">else
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">  jj bookmark move --to &#34;</span><span style="color:#a6e22e">closest_pushable</span><span style="color:#960050;background-color:#1e0010">(@)</span><span style="color:#e6db74">&#34; &#34;</span><span style="color:#960050;background-color:#1e0010">$@</span><span style="color:#e6db74">&#34;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">fi
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">&#34;&#34;&#34;</span>, <span style="color:#e6db74">&#34;&#34;</span>]
</span></span></code></pre></div><p>This version of tug works just like the previous one if no argument is given. But if you do pass an argument, it will move the bookmark with the name that you passed instead of the closest one.</p>
<p>How about if you’ve just pushed to GitHub, and you want to create a pull request from that pushed bookmark? The <code>gh pr create</code> command isn’t smart enough to figure that out automatically, but you can tell it which bookmark to use:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-Toml" data-lang="Toml"><span style="display:flex;"><span><span style="color:#a6e22e">pr</span> = [<span style="color:#e6db74">&#34;util&#34;</span>, <span style="color:#e6db74">&#34;exec&#34;</span>, <span style="color:#e6db74">&#34;--&#34;</span>, <span style="color:#e6db74">&#34;bash&#34;</span>, <span style="color:#e6db74">&#34;-c&#34;</span>, <span style="color:#e6db74">&#34;&#34;&#34;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">gh pr create --head $(jj log -r &#39;closest_bookmark(@)&#39; -T &#39;bookmarks&#39; --no-graph | cut -d &#39; &#39; -f 1)
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">&#34;&#34;&#34;</span>]
</span></span></code></pre></div><p>Just grab the list of bookmarks attached to the closest bookmark, take the first one, pass it to <code>gh pr create</code>, and you’re all set.</p>
<p>What if you just want single commands that let you work against a git remote, with defaults tuned for automatic tugging, pushing, and tracking? I’ve also got you covered.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-Toml" data-lang="Toml"><span style="display:flex;"><span><span style="color:#a6e22e">init</span> = [<span style="color:#e6db74">&#34;util&#34;</span>, <span style="color:#e6db74">&#34;exec&#34;</span>, <span style="color:#e6db74">&#34;--&#34;</span>, <span style="color:#e6db74">&#34;bash&#34;</span>, <span style="color:#e6db74">&#34;-c&#34;</span>, <span style="color:#e6db74">&#34;&#34;&#34;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">jj git init --colocate
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74"># only track origin branches, not upstream or others
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">jj bookmark track &#39;glob:*@origin&#39;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">&#34;&#34;&#34;</span>]
</span></span></code></pre></div><p>Use <code>jj init</code> to colocate jj into this git repo, and then track any branches from upstream, like you would get from a git clone.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-Toml" data-lang="Toml"><span style="display:flex;"><span><span style="color:#a6e22e">pull</span> = [<span style="color:#e6db74">&#34;util&#34;</span>, <span style="color:#e6db74">&#34;exec&#34;</span>, <span style="color:#e6db74">&#34;--&#34;</span>, <span style="color:#e6db74">&#34;bash&#34;</span>, <span style="color:#e6db74">&#34;-c&#34;</span>, <span style="color:#e6db74">&#34;&#34;&#34;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74"># Find the closest bookmark
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">closest=&#34;</span><span style="color:#960050;background-color:#1e0010">$(</span><span style="color:#a6e22e">jj</span> <span style="color:#a6e22e">log</span> <span style="color:#a6e22e">-r</span> <span style="color:#e6db74">&#39;closest_bookmark(@)&#39;</span> <span style="color:#960050;background-color:#1e0010">\</span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">-n</span> <span style="color:#ae81ff">1</span> <span style="color:#a6e22e">-T</span> <span style="color:#e6db74">&#39;bookmarks&#39;</span> <span style="color:#a6e22e">--no-graph</span> <span style="color:#960050;background-color:#1e0010">|</span> <span style="color:#a6e22e">cut</span> <span style="color:#a6e22e">-d</span> <span style="color:#e6db74">&#39; &#39;</span> <span style="color:#a6e22e">-f</span> <span style="color:#ae81ff">1</span><span style="color:#960050;background-color:#1e0010">)</span><span style="color:#e6db74">&#34;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74"># Remove the trailing * from the name if there is one
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">closest=&#34;</span><span style="color:#960050;background-color:#1e0010">$</span>{<span style="color:#a6e22e">closest</span><span style="color:#960050;background-color:#1e0010">%\\*</span>}<span style="color:#e6db74">&#34;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74"># Now fetch from the git remote
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">jj git fetch
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74"># If the closest bookmark still exists, rebase on it (else trunk)
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">jj log -n 1 -r &#34;</span><span style="color:#960050;background-color:#1e0010">$</span>{<span style="color:#a6e22e">closest</span>}<span style="color:#e6db74">&#34; 2&gt;&amp;1 &gt; /dev/null \
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">  &amp;&amp; jj rebase -d &#34;</span><span style="color:#960050;background-color:#1e0010">$</span>{<span style="color:#a6e22e">closest</span>}<span style="color:#e6db74">&#34; || jj rebase -d &#39;trunk()&#39;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74"># Show the new state of things after the pull
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">jj log -r &#39;stack()&#39;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">&#34;&#34;&#34;</span>]
</span></span></code></pre></div><p>Then, you can <code>jj pull</code> to find the closest bookmark to <code>@</code>, do a git fetch, rebase your current local commits on top of whatever just got pulled, and then show your new stack. When you’re done, just <code>jj push</code>.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-Toml" data-lang="Toml"><span style="display:flex;"><span><span style="color:#a6e22e">push</span> = [<span style="color:#e6db74">&#34;util&#34;</span>, <span style="color:#e6db74">&#34;exec&#34;</span>, <span style="color:#e6db74">&#34;--&#34;</span>, <span style="color:#e6db74">&#34;bash&#34;</span>, <span style="color:#e6db74">&#34;-c&#34;</span>, <span style="color:#e6db74">&#34;&#34;&#34;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74"># Check to see if we can tug a bookmark to @
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">tuggable=&#34;</span><span style="color:#960050;background-color:#1e0010">$(</span><span style="color:#a6e22e">jj</span> <span style="color:#a6e22e">log</span> <span style="color:#a6e22e">-r</span> <span style="color:#e6db74">&#39;closest_bookmark(@)..closest_pushable(@)&#39;</span> <span style="color:#960050;background-color:#1e0010">\</span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">-T</span> <span style="color:#e6db74">&#39;&#34;n&#34;&#39;</span> <span style="color:#a6e22e">--no-graph</span><span style="color:#960050;background-color:#1e0010">)</span><span style="color:#e6db74">&#34;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74"># If we can, tug that bookmark as close to @ as possible
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">[[ -n &#34;</span><span style="color:#960050;background-color:#1e0010">$</span><span style="color:#a6e22e">tuggable</span><span style="color:#e6db74">&#34; ]] &amp;&amp; jj tug
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74"># Now find the closest thing that we can push to `origin`
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">pushable=&#34;</span><span style="color:#960050;background-color:#1e0010">$(</span><span style="color:#a6e22e">jj</span> <span style="color:#a6e22e">log</span> <span style="color:#a6e22e">-r</span> <span style="color:#e6db74">&#39;remote_bookmarks(remote=origin)..@&#39;</span> <span style="color:#960050;background-color:#1e0010">\</span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">-T</span> <span style="color:#e6db74">&#39;bookmarks&#39;</span> <span style="color:#a6e22e">--no-graph</span><span style="color:#960050;background-color:#1e0010">)</span><span style="color:#e6db74">&#34;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74"># If we have something to push, run `jj git push`
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">[[ -n &#34;</span><span style="color:#960050;background-color:#1e0010">$</span><span style="color:#a6e22e">pushable</span><span style="color:#e6db74">&#34; ]] &amp;&amp; jj git push || echo &#34;</span><span style="color:#a6e22e">Nothing</span> <span style="color:#a6e22e">to</span> <span style="color:#a6e22e">push</span>.<span style="color:#e6db74">&#34;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74"># Now that we have pushed, find the closest bookmark and remove *
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">closest=&#34;</span><span style="color:#960050;background-color:#1e0010">$(</span><span style="color:#a6e22e">jj</span> <span style="color:#a6e22e">log</span> <span style="color:#a6e22e">-r</span> <span style="color:#e6db74">&#39;closest_bookmark(@)&#39;</span> <span style="color:#a6e22e">-n</span> <span style="color:#ae81ff">1</span> <span style="color:#a6e22e">-T</span> <span style="color:#e6db74">&#39;bookmarks&#39;</span> <span style="color:#960050;background-color:#1e0010">\</span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">--no-graph</span> <span style="color:#960050;background-color:#1e0010">|</span> <span style="color:#a6e22e">cut</span> <span style="color:#a6e22e">-d</span> <span style="color:#e6db74">&#39; &#39;</span> <span style="color:#a6e22e">-f</span> <span style="color:#ae81ff">1</span><span style="color:#960050;background-color:#1e0010">)</span><span style="color:#e6db74">&#34;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">closest=&#34;</span><span style="color:#960050;background-color:#1e0010">$</span>{<span style="color:#a6e22e">closest</span><span style="color:#960050;background-color:#1e0010">%\\*</span>}<span style="color:#e6db74">&#34;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74"># Check to see if that bookmark is already tracking origin
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">tracked=&#34;</span><span style="color:#960050;background-color:#1e0010">$(</span><span style="color:#a6e22e">jj</span> <span style="color:#a6e22e">bookmark</span> <span style="color:#a6e22e">list</span> <span style="color:#a6e22e">-r</span> <span style="color:#960050;background-color:#1e0010">$</span>{<span style="color:#a6e22e">closest</span>} <span style="color:#a6e22e">-t</span> <span style="color:#960050;background-color:#1e0010">\</span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">-T</span> <span style="color:#e6db74">&#39;if(remote == &#34;origin&#34;, name)&#39;</span><span style="color:#960050;background-color:#1e0010">)</span><span style="color:#e6db74">&#34;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74"># If that bookmark isn&#39;t tracking origin, start to track origin
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">[[ &#34;</span><span style="color:#960050;background-color:#1e0010">$</span><span style="color:#a6e22e">tracked</span><span style="color:#e6db74">&#34; == &#34;</span><span style="color:#960050;background-color:#1e0010">$</span><span style="color:#a6e22e">closest</span><span style="color:#e6db74">&#34; ]] \
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">  || jj bookmark track &#34;</span><span style="color:#960050;background-color:#1e0010">$</span>{<span style="color:#a6e22e">closest</span>}<span style="color:#960050;background-color:#1e0010">@</span><span style="color:#a6e22e">origin</span><span style="color:#e6db74">&#34;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">&#34;&#34;&#34;</span>]
</span></span></code></pre></div><p>This push handles looking for a tuggable bookmark, tugging it, doing a git push, and making sure that you’re tracking the origin copy of whatever you just pushed, in case you created a new branch.</p>
<h3 id="further-reading">further reading</h3>
<p>For another perspective on jj configuration, partly overlapping with this post, check out my JJ Con talk, <a href="/2025/09/28/stupid-jj-tricks/">stupid jj tricks</a>.</p>
<p>You can also try reading some jj config files directly, like <a href="https://github.com/indirect/dotfiles/blob/main/private_dot_config/private_jj/config.toml">my jj config</a>, or <a href="https://gist.github.com/thoughtpolice/8f2fd36ae17cd11b8e7bd93a70e31ad6">thoughtpolice&rsquo;s jj config</a>, or <a href="https://gist.github.com/pksunkara/622bc04242d402c4e43c7328234fd01c">pksunkara&rsquo;s jj config</a>.</p>
<h3 id="next-time">next time</h3>
<p>Hopefully this tour through jj configuration options has revealed some ways that jj can be used to do more than was possible with only git. Next time, we&rsquo;ll focus on the ways that jj goes beyond git, offering things that were impractical or even impossible before.</p>
]]></content></entry><entry><title type="html">&lt;code>jj&lt;/code> part 3: workflows</title><link href="https://andre.arko.net/2025/10/12/jj-part-3-workflows/"/><updated>2025-10-12T10:52:56+09:00</updated><id>http://andre.arko.net/2025/10/12/jj-part-3-workflows/</id><content type="html"><![CDATA[<p>Previously on this blog: <a href="/2025/10/02/jj-part-2-commands/"><code>jj</code> part 2: commands</a></p>
<p>Now that you hopefully have an idea of how to operate jj, let’s look at the commands you need to get work done in jj. One great aspect of jj layering on top of git repos is that the git repo is still there underneath, and you can use any git command exactly like you usually would if there’s anything missing from your jj workflows.</p>
<h3 id="submit-a-pull-request">submit a pull request</h3>
<p>The flow to create and send a PR will probably look pretty familiar: use <code>jj git clone</code> to get a copy of the repo, make your changes, use <code>jj commit</code> to create your new commits. When you’re ready, use <code>jj bookmark set NAME</code> to give your changes a name and <code>jj git push</code> to create a new branch on the remote. Use GitHub.com or <code>gh pr create --head NAME</code> to open the PR.</p>
<p>If you amend the commits in your PR, you can force-push the new commits with <code>jj git push</code>. If you add new changes on top, you’ll need to <code>jj bookmark set NAME</code> to update the bookmark to the latest change before you <code>jj git push</code> again.</p>
<p>That’s the whole flow! Congratulations on migrating from git to jj for your everyday work.</p>
<p>If using <code>bookmark set</code> all the time gets tedious, there’s a community alias named <code>jj tug</code> that finds the closest bookmark and moves it to the closest pushable change. I personally wrote an alias for myself named <code>jj push</code> that I use to handle pushing new changes to existing remote branches. We’ll talk about those aliases in the next major section, which is about configuring jj.</p>
<h3 id="work-on-multiple-prs-at-once">work on multiple PRs at once</h3>
<p>One situation I often find myself in is working on two (or even more) pull requests at the same time. With the powerful commit-editing primitives provided by jj, there are at least two (and probably more) ways to structure this kind of parallel work.</p>
<p>The first option is what I think of as merge-based: create a merge commit that unifies the tips of your two or more branches using <code>jj new -d A -d B</code>, do your work, and create new commits with <code>jj split</code> or <code>jj commit</code>. Then,  rebase those commits using <code>jj rebase -r @- --insert-before A</code> or the like, moving the new commits backwards into one of the PR branches. This is the same as the “megamerge” strategy described above, but it works just as well with two branches.</p>
<p>The second option is to liberally rebase every branch on top of each other, creating a completely linear history where PR #4 contains PR #3, which also contains PR #2, which also contains PR #1. Since jj uses change IDs to keep track of changes as their commits are amended or rebased, you can rebase the entire chain on top of new commits to <code>main</code>. Your bookmarks will stay in the same place, and you can <code>jj git push</code> to update each remote branch. Any time one of the PRs lands, you rebase your full chain on top of the new <code>main</code> and resume work where you left off.</p>
<p>If you want to work on multiple branches at once, you will probably also find the articles <a href="https://ofcr.se/jujutsu-merge-workflow">Jujutsu Merge Workflow</a> and <a href="https://v5.chriskrycho.com/journal/jujutsu-megamerges-and-jj-absorb/">Jujutsu Megamerges and <code>jj absorb</code></a> interesting.</p>
<h3 id="further-workflow-reading">further workflow reading</h3>
<p>There are many new workflows that jj users have already developed, and this brief overview is just the tip of the iceberg. The jj docs include a section on <a href="https://jj-vcs.github.io/jj/latest/github/">using jj with GitHub or GitLab</a>, and there are some great reflections on different workflows in the blog posts  <a href="https://kubamartin.com/posts/introduction-to-the-jujutsu-vcs/">Jujutsu VCS Introduction and Patterns</a>, <a href="https://pksunkara.com/thoughts/git-experts-should-try-jujutsu/">Git experts should try Jujutsu</a>, and <a href="https://zerowidth.com/2025/jj-tips-and-tricks/">jj tips and tricks</a>.</p>
<h3 id="next-time">next time</h3>
<p>Next up, we&rsquo;re going to talk about configuring jj. Want to use a diff tool? A merge tool? Add your own commands? Optimize your day to day work? We&rsquo;ve got options.</p>
<p>Continue with <a href="/2025/10/15/jj-part-4-configuration/"><code>jj</code> part 4: configuration</a>.</p>
]]></content></entry><entry><title type="html">Announcing &lt;code>rv&lt;/code> 0.2</title><link href="https://andre.arko.net/2025/10/11/announcing-rv-0.2/"/><updated>2025-10-11T01:48:20-07:00</updated><id>http://andre.arko.net/2025/10/11/announcing-rv-0.2/</id><content type="html"><![CDATA[<p>With the help of many new contributors, and after many late nights wrestling with make, we are happy to (slightly belatedly) announce <a href="https://github.com/spinel-coop/rv/releases/tag/v0.2.0">the 0.2 release of rv</a>!</p>
<p>This version dramatically expands support for Rubies, shells, and architectures.</p>
<p>Rubies: we have added Ruby 3.3, as well as re-compiled all Ruby 3.3 and 3.4 versions with YJIT. On Linux, YJIT increases our glibc minimum version to 2.35 or higher. That means most distro releases from 2022 or later should work, but please let us know if you run into any problems.</p>
<p>Shells: we have added support for bash, fish, and nushell in addition to zsh.</p>
<p>Architectures: we have added Ruby compiled for macOS on x86, in addition to Apple Silicon, and added Ruby compiled for Linux on ARM, in addition to x86.</p>
<p>Special thanks to newest member of the maintainers&rsquo; team <a href="https://github.com/adamchalmers">@adamchalmers</a> for improving code and tests, adding code coverage and fuzzing, heroic amounts of issue triage, and nushell support. Additional thanks are due to all the new contributors in version 0.2, including <a href="https://github.com/Thomascountz">@Thomascountz</a>, <a href="https://github.com/lgarron">@lgarron</a>, <a href="https://github.com/coezbek">@coezbek</a>, and <a href="https://github.com/renatolond">@renatolond</a>.</p>
<p>To upgrade, run <code>brew install rv</code>, or check the <a href="https://github.com/spinel-coop/rv/releases/tag/v0.2.0">release notes</a> for other options.</p>
]]></content></entry><entry><title type="html">The RubyGems “security incident”</title><link href="https://andre.arko.net/2025/10/09/the-rubygems-security-incident/"/><updated>2025-10-09T19:45:15-07:00</updated><id>http://andre.arko.net/2025/10/09/the-rubygems-security-incident/</id><content type="html"><![CDATA[<p>Ruby Central posted an extremely concerning &ldquo;<a href="https://rubycentral.org/news/rubygems-org-aws-root-access-event-september-2025/">Incident Response Timeline</a>&rdquo; today, in which they make a number of exaggerated or purely misleading claims. Here&rsquo;s my effort to set the record straight.</p>
<p>First, and most importantly: <strong>I was a primary operator of RubyGems.org, securely and successfully, for over ten years. Ruby Central does not accuse me of any harms or damages in their post, in fact stating “we have no evidence to indicate that any RubyGems.org data was copied or retained by unauthorized parties, including Mr. Arko.”</strong></p>
<p>The actions I took during a time of great confusion and uncertainty (created by Ruby Central!) were careful, specific, and aimed to defend both Ruby Central the organization and RubyGems.org the service from potential threats.</p>
<p>The majority of the team, including developers in the middle of paid full-time work for Ruby Central, had just had all of their permissions on GitHub revoked. And then restored six days later. And then revoked again the next day. Even after the second mass-deletion of team permissions, Marty Haught sent an email to the team within minutes, at 12:47pm PDT, saying he was (direct quote) &ldquo;terribly sorry&rdquo; and &ldquo;I messed up&rdquo;. <small><strong>Update</strong>: Added email timestamp.</small></p>
<p>The erratic and contradictory communication supplied by Marty Haught, and the complete silence from Shan and the board, made it impossible to tell exactly who had been authorized to take what actions. As this situation occurred, I was the primary on-call. My contractual, paid responsibility to Ruby Central was to defend the RubyGems.org service against potential threats. </p>
<p>Marty&rsquo;s final email clearly stated &ldquo;I&rsquo;ll follow up more on this and engage with the governance rfc in good faith.&rdquo;. Just a few minutes after that email, at 1:01pm PDT, Marty also posted <a href="https://github.com/rubygems/rfcs/pull/61#issuecomment-3309461815">a public GitHub comment</a>, where he agreed to participate in the proposed governance process and stated &ldquo;I&rsquo;m committed to find the right governance model that works for us all. More to come.&rdquo; <small style="display: inline;"><strong>Update</strong>: screenshot of comment removed and replaced with link, since the comment appears to still be visible (at least to logged out users) on GitHub.</small></p>
<p>Given Marty&rsquo;s claims, the sudden permission deletions made no sense. Worried about the possibility of hacked accounts or some sort of social engineering, I took action as the primary on-call engineer to lock down the AWS account and prevent any actions by possible attackers. I did not change the email addresses on any accounts, leaving them all owned by a team-shared email at rubycentral.org, to ensure the organization retained overall control of the accounts, even if individuals were somehow taking unauthorized actions.</p>
<p>Within a couple of days, Ruby Central made an (unsigned) public statement, and various board members agreed to talk directly to maintainers. At that point, I realized that what I thought might have been a malicious takeover was both legitimate and deliberate, and Marty would never &ldquo;fix the permissions structure&rdquo;, or &ldquo;follow up more&rdquo; as he said.</p>
<p>Once I understood the situation, I backed off to let Ruby Central take care of their &ldquo;security audit&rdquo;. I left all accounts in a state where they could recover access. I did not alter, or try to alter, anything in the Ruby Central systems or GitHub repository after that. I was confident, at the time, that Ruby Central&rsquo;s security experts would quickly remove all outside access.</p>
<p>My confidence was sorely misplaced.</p>
<p>Almost two weeks later, someone asked if I still had access and I discovered (to my great alarm), that Ruby Central&rsquo;s &ldquo;security audit&rdquo; had failed. Ruby Central also had not removed me as an &ldquo;owner&rdquo; of the Ruby Central GitHub Organization. They also had not rotated any of the credentials shared across the operational team using the RubyGems 1Password account.</p>
<p>I believe Ruby Central confused themselves into thinking the &ldquo;Ruby Central&rdquo; 1Password account was used by operators, and they did revoke my access there. However, that 1Password account was not used by the open source team of RubyGems.org service operators. Instead, we used the &ldquo;RubyGems&rdquo; 1Password account, which was full of operational credentials. Ruby Central did not remove me from the &ldquo;RubyGems&rdquo; 1Password account, even as of today.</p>
<p>Aware that I needed to disclose this surprising access, but also aware that it was impossible for anyone except former operators to exploit this security failure, I immediately wrote an email to Ruby Central to disclose the problem.</p>
<p>Here is a copy of my disclosure email, in full.</p>
<pre tabindex="0"><code>From: André Arko &lt;andre@arko.net&gt;
Subject: Re: RubyGems.org access
Date: September 30, 2025 at 10:23:12 AM PDT
To: Marty Haught &lt;marty@rubycentral.org&gt;

Hi Marty,

It has come to my attention that despite the statements in [your] email, I have had uninterrupted access to RubyGems.org production environments from September 18 until today, September 30, via the root credentials of the Ruby Central AWS account, as well as continued and ongoing access to the full feed of production alerts and logs in DataDog.

It seems that the only permissions I have had removed are from the GitHub organization named &#34;rubygems&#34;, which as you know is unrelated to the RubyGems.org production access you mention in your email.

I have also noticed I am still, as of September 30, the owner of the GitHub organizations named &#34;rubycentral&#34; and &#34;rubytogether&#34;.

I am unable to transfer the HelpScout or PagerDuty accounts, as you have disabled my andre@rubygems.org Google account.

Please advise as to your desired resolution of this situation.

Thank you,
André Arko
</code></pre><p>Ruby Central did not reply to this email for over three days.</p>
<p>When they finally did reply, they seem to have developed some sort of theory that I was interested in &ldquo;access to PII&rdquo;, which is entirely false. <strong>I have no interest in any PII, commercially or otherwise</strong>. As my private email published by Ruby Central demonstrates, my entire proposal was based solely on company-level information, with no information about individuals included in any way. Here&rsquo;s their response, over three days later.</p>
<pre tabindex="0"><code>From: Marty Haught &lt;marty@rubycentral.org&gt;
Subject: Re: RubyGems.org access
Date: October 3, 2025 at 6:54:01 PM MDT
To: André Arko &lt;andre@arko.net&gt;

Hi André,

Please confirm that you cannot access the Ruby Central AWS root account credentials, either through the console or by access keys.

In addition, please confirm whether you are in possession of any RubyGems.org production data,  including, but not limited to, server logs, access logs, PII, or other organizational data.

Thank you,
Marty
</code></pre><p>In addition to ignoring the (huge) question of how Ruby Central failed to secure their AWS Root credentials for almost two weeks, and <strong>appearing to only be aware of it because I reported it to them</strong>, their reply also failed to ask whether any other shared credentials might still be valid. There were more.</p>
<pre tabindex="0"><code>From: André Arko &lt;andre@arko.net&gt;
Subject: Re: RubyGems.org access
Date: October 5, 2025 at 11:59:35 AM PDT
To: Marty Haught &lt;marty@rubycentral.org&gt;

Hi Marty,

Thanks for letting me know you got my email disclosing my unintended access. I’m concerned that security must not be a very high priority for Ruby Central since no one acknowledged my disclosure for more than three days, but I appreciate the confirmation.

As far as I can tell, I can no longer access the Ruby Central AWS root account either through the console or via access keys.

I confirm I did not download or save any production data after your email of September 18, including server logs, access logs, PII, or other organizational data.

However, while checking AWS credentials in order to write this email, I discovered that several other service credentials have not been rotated, and are still valid for production AWS access. That means both myself and the other former operators all still have access to AWS via those previously-shared credentials.

I would appreciate it if you could answer the request from my first email, and reply with your desired resolution for this remaining unintended production access, as well as the GitHub organization ownership.

Thanks,
André
</code></pre><p>Unbeknownst to me, while I was answering Marty&rsquo;s email in good faith, Ruby Central&rsquo;s attorney was sending my lawyer a letter alleging I had committed a federal crime, on the theory that I had &ldquo;hacked&rdquo; Ruby Central&rsquo;s AWS account. On the contrary, my actions were taken in defense of the service that Ruby Central was paying me to support and defend.</p>
<p>With my side of the story told, I&rsquo;ll leave it to you to decide whether you think it&rsquo;s true that &ldquo;Ruby Central remains committed to transparent, responsible stewardship of the RubyGems infrastructure and to maintaining the security and trust that the Ruby ecosystem depends on.&rdquo;</p>
]]></content></entry><entry><title type="html">Announcing gem.coop, a community gem server</title><link href="https://andre.arko.net/2025/10/05/announcing-gem-coop/"/><updated>2025-10-05T19:00:00-08:00</updated><id>http://andre.arko.net/2025/10/05/announcing-gem-coop/</id><content type="html"><![CDATA[<p>The team behind the last ten years of rubygems.org, including @deivid-rodriguez, @duckinator, @martinemde, @segiddins, @simi, and myself, is very pleased to announce a new gem server for the Ruby community: <a href="https://gem.coop">gem.coop</a>.</p>
<p>The new server’s governance policies are being prepared in coordination with Mike McQuaid of Homebrew, and will be released later this week.</p>
<p>The current versions of RubyGems and Bundler work with this new server already, and any Ruby developer is welcome to switch to using this new server immediately.</p>
<p>We have exciting plans to add new features and functionality in the coming days. <a href="https://gem.coop">Join us!</a></p>
]]></content></entry><entry><title type="html">&lt;code>jj&lt;/code> part 2: commands &amp; revsets</title><link href="https://andre.arko.net/2025/10/02/jj-part-2-commands/"/><updated>2025-10-02T10:52:56+09:00</updated><id>http://andre.arko.net/2025/10/02/jj-part-2-commands/</id><content type="html"><![CDATA[<p>Previously on this blog: <a href="/2025/09/28/jj-part-1-what-is-it/"><code>jj</code> part 1: what is it</a></p>
<p>Now, let’s take a look at the most common jj commands, with a special focus on the way arguments are generally consistent and switches don’t hide totally different additional commands.</p>
<h3 id="jj-log">jj log</h3>
<p>The log command is the biggest consumer of revsets, which are passed using <code>-r</code> or <code>--revisions</code>. With <code>@</code>, which is the jj version of <code>HEAD</code>, you can build a revset for exactly the commits you want to see. The git operator <code>..</code> is supported, allowing you to log commits after A and up to B with <code>-r A..B</code>, but that’s just the start. Here’s a quick list of some useful revsets to give you the flavor:</p>
<ul>
<li><code>@-</code> the parent of the current commit</li>
<li><code>kv+</code> the first child of the change named <code>kv</code></li>
<li><code>..A &amp; ..B</code> changes in the intersection of <code>A</code> and <code>B</code>’s ancestors</li>
<li><code>~description(glob:&quot;wip:\*&quot;)</code> changes whose message does <em>not</em> start with <code>wip:</code>, because tilde negates a revset</li>
<li><code>heads(::@ &amp; mutable() &amp; ~description(exact:&quot;&quot;) &amp; (~empty() | merges()))</code> the closest “pushable” change, meaning the nearest ancestor of <code>@</code> that is mutable (by default mutable means “not in the main/trunk branch”), that has some description set, and that either has some changes or is a merge commit. (Some jj merge commits can be empty, if there were no conflicts.)</li>
</ul>
<p>Using the jj config file, you can give any revset an alias, and then use that alias. I use <code>closest_pushable(@)</code> quite a bit, especially when naming branches and pushing.</p>
<p>For a full review of everything that’s possible with revsets, check out <a href="https://jj-vcs.github.io/jj/latest/revsets/">the revset documentation</a> and the blog post <a href="https://willhbr.net/2024/08/18/understanding-revsets-for-a-better-jj-log-output/">Understanding Revsets for a Better JJ Log Output</a>.</p>
<h3 id="jj-commit--desc--new--edit--split">jj commit / desc / new / edit / split</h3>
<p>The functionality of <code>git commit</code> is broken up into four separate jj commands. You use <code>new</code> to create a new empty child change, defaulting to <code>@</code>, and edit it. The <code>desc</code> command lets you set the description (or message) on a given change. The <code>commit</code> command works like git, but is effectively the same as <code>jj desc &amp;&amp; jj new</code>. You use <code>edit</code> to re-open an existing change for amending, and <code>split</code> to interactively select a diff to break out into a second change. These are all common git workflows, done by using flags or multiple git commands, made direct and straightforward single commands in jj.</p>
<h3 id="jj-restore--abandon">jj restore / abandon</h3>
<p>What if using <code>checkout</code> and <code>reset</code> to roll back either files or full commits had clearer names?</p>
<p>If you want to get back a file from a previous change, you can use <code>restore</code>. Specify which change you want to bring back, and also provide a file name or glob to limit the restoration to specific files.</p>
<p>Where you might have previously used <code>git reset</code> or <code>git checkout</code> to manipulate which commits are included in the current branch, you can now use <code>abandon</code> to remove entire changes from your history. Without any arguments it will remove <code>@</code>, the working commit, which is similar to <code>git reset --hard</code>. With arguments, <code>abandon</code> will remove all changes in the given revset from the local history.</p>
<h3 id="jj-bookmark-list--set--track">jj bookmark list / set / track</h3>
<p>Bookmarks are jj’s alternative to named git branches, and can be set up to automatically track a branch in a git remote. While compatibility with git branches is nice, names aren’t required by jj’s model. You can push your current unnamed change instantly with <code>jj git push --change @</code>, and jj will use the change ID (which stays the same across amends and rebases) as the git branch name. Now you don’t have to think of a good name for your branch before you can work on it (or push it!).</p>
<p>For more detail comparing and contrasting bookmarks to branches, I recommend the post <a href="https://neugierig.org/software/blog/2025/08/jj-bookmarks.html">Understanding Jujutsu bookmarks</a>.</p>
<h3 id="jj-git-push--fetch">jj git push / fetch</h3>
<p>It does what you would expect based on git, but the defaults are different than you might expect. Unless you configure the <code>git.fetch</code> and <code>git.push</code> settings, jj will only push to or fetch from <code>origin</code>. To operate on another remote, pass <code>--remote NAME</code>. To operate on all remotes, use <code>glob:*</code> as the remote name.</p>
<h3 id="jj-rebase--squash">jj rebase / squash</h3>
<p>The rebase command works like you would expect, but better. You can rebase a  single change to a different place with <code>jj rebase -r id --insert-before A</code>, or rebase a change and all it’s descendants with <code>jj rebase -s id --insert-after B</code>. You can even rebase an entire branch automatically with <code>jj rebase -b @ --destination C</code>, moving every ancestor of <code>@</code> that is not an ancestor of <code>C</code> into a new chain of commits descending from <code>C</code>. I did all of these constantly in git, and it’s much more involved.</p>
<p>The squash command is just a clear, single command for the common git operation where you move a diff into a commit or move a diff out of a commit, by change ID and/or filename.</p>
<h3 id="jj-merge-doesnt-exist">jj merge (doesn’t exist)</h3>
<p>The git rebase and merge commands (also including apply-patch, cherry-pick, and others) are all a bit special because they can create conflicts that have to be resolved before git will allow the commit to be… committed. This is the other half of the magic of jj: your new commit just holds any conflicts inside it. It’s impossible to lose work in a merge disaster because everything is always committed. You can resolve conflicts immediately, after other merges, or never! The results are always immediately stored, no matter how complete or incomplete your resolution is at the time.</p>
<p>Thanks to this feature, you don’t need a dedicated merge command—any new change can have however many parents you want, regardless of conflicts. It’s just as valid to <code>jj new A B C D E</code> as it is to <code>jj new A</code>. One pattern that is common in jj but was miserable in git is to create a “megamerge” combining all your current work branches. All editing happens on top of the megamerge, and you move individual changes backwards into a specific branch as you decide where to put them. Compared to git, it feels like magic.</p>
<h3 id="commands-beyond-git">commands beyond git</h3>
<p>There are many jj commands that have no analogous git command. Some real standouts include <code>jj absorb</code>, <code>jj parallelize</code>, and <code>jj undo</code>. We’ll talk more about those commands in a future post about jj beyond git.</p>
<h3 id="further-command-reading">further command reading</h3>
<p>The previously mentioned <a href="https://justinpombrio.net/src/jj-cheat-sheet.pdf">jj cheat sheet PDF</a> has a second page, containing a quick summary of each command, what it does, and the arguments it accepts.</p>
<h3 id="next-time">next time</h3>
<p>Now that we have talked about commands, next up is workflows! How can you use jj to work on a pull request? How can you work on multiple branches or PRs at the same time?</p>
<p>Continue with <a href="/2025/10/12/jj-part-3-workflows/"><code>jj</code> part 3: workflows</a>.</p>
<p>The full series also includes: <a href="/2025/10/15/jj-part-4-configuration/">part 4: configuration</a></p>
]]></content></entry><entry><title type="html">&lt;code>rv&lt;/code>, a Ruby manager for the future</title><link href="https://andre.arko.net/2025/09/30/rv-a-ruby-manager-for-the-future/"/><updated>2025-09-30T12:25:36-07:00</updated><id>http://andre.arko.net/2025/09/30/rv-a-ruby-manager-for-the-future/</id><content type="html"><![CDATA[<p><small>This post was originally given as a talk at the <a href="https://luma.com/sfruby">SF Ruby Meetup</a>. The <a href="https://speakerdeck.com/indirect/rv-a-ruby-manager-for-the-future">slides</a> are also available.</small></p>
<script defer class="speakerdeck-embed" data-id="4591b4b2d21d42c399dea04572cc8cff" data-ratio="1.7777777777777777" src="//speakerdeck.com/assets/embed.js"></script>
<p>For the last ten years or so of working on Bundler, I’ve had a wish rattling around: I want a bigger, better dependency manager. It doesn’t just manage your gems, it manages your ruby versions, too. It doesn’t just manage your ruby versions, it installs pre-compiled rubies so you don’t have to wait for ruby to compile from source over and over. And more than all of that, it makes it completely trivial to run any script or tool written in ruby, even if that script or tool needs a different ruby and gems than your application does.</p>
<p>For the entire ten years of daydreaming, I’ve been hoping someone else would build it and I could just use it. Then I discovered that someone <em>did</em> build it… but for Python. It’s called <a href="https://docs.astral.sh/uv/"><code>uv</code></a>, and almost exactly one year ago <a href="https://astral.sh/blog/uv-unified-python-packaging">version 0.3 shipped</a> with all the features I had wished for, and even more that I hadn’t thought to wish for.</p>
<p>At this point, I’ve been using <code>uv</code> for almost a year and every time I use a project written in Python, the experience is delightful. Not only can you run a command directly out of any package that isn’t even installed, you can run a command that requires a python you don’t have installed, and <code>uv</code> takes care of installing the right python, installing the right packages, and running your command, in just a second or two.</p>
<p>Whether you want to run a CLI tool, a webapp, or a random script, <code>uv</code> always ensures the environment is correct as part of running the command. No more installing a new package version only to realize later you broke something old, no more setting up dependencies manually only to have the script running inside cron silently break later.</p>
<p>Earlier this year, my long time consulting job disappeared and I found myself looking for something to replace it. One of my ideas was to start a company inspired by <a href="https://geomys.org">Geomys</a> in the Go language, offering expert advice from open source maintainers, but the idea felt weak to me without a “spotlight” project to show off our expertise.</p>
<p>In July of this year, I finally realized that these two ideas could go together extremely well—the company can show our expertise by building this developer tool, and clients paying for our advice to solve their problems can ensure we are able to support and expand the tool.</p>
<p>I talked to some Ruby friends about the idea, and it resonated with them, so we started working on both the company and the open source project. Today, Spinel Cooperative has a website at <a href="https://spinel.coop">spinel.coop</a>, and <code>rv</code> has a website at <a href="https://rv.dev">rv.dev</a>. The team has expanded, and includes notable RubyGems and Bundler contributors <a href="https://segiddins.me">Samuel Giddins</a> and <a href="https://github.com/deivid-rodriguez">David Rodriguez</a>, notable Rails contributors <a href="https://kaspth.com">Kasper Timm Hanson</a> and <a href="https://sls.name">Sam Stephenson</a>, who is also the original creator of <a href="https://rbenv.org">rbenv</a> and ruby-build.</p>
<p>Our goal is a completely new kind of management tool, where you don’t need to install rvm and then some ruby and then update rubygems and bundler and then bundle install your gems—you just run your command, and everything is handled. Not a version manager, or a dependency manager, but both of those things and much more.</p>
<p>With that vision in place, we were now faced with a very practical question. What can we build that would be useful right away? We landed on precompiled rubies for development work as the most useful place to start, and got to work.</p>
<p>We&rsquo;re using Rust to build <code>rv</code>, for two reasons. The obvious reason is that Rust produces very fast results, which is also why our biggest inspiration <code>uv</code> is written in Rust. The less obvious reason is based on years of trying to onboard new contributors to Bundler and RubyGems—it turns out if you are a Ruby developer, you unfortunately don&rsquo;t (yet) know the subset of Ruby that we are forced to use for Bundler and RubyGems.</p>
<p>There are two major things that basically every Ruby program does that you can&rsquo;t do if you are managing gems. First, you can&rsquo;t use any gems. If you want to use code that&rsquo;s inside a gem, you need to copy that code wholesale into Bundler or RubyGems, and then you need to constantly update it anytime that gem has any changes. Second, you can&rsquo;t use anything with native extensions, ever. JSON gem? Psych gem for YAML? Completely impossible, because Bundler and RubyGems need to be installable even if there is no compiler present.</p>
<p>So with those constraints in mind, and with a clear goal in mind of a tool so fast you normally can&rsquo;t even tell it&rsquo;s running, we settled on Rust, and started building a CLI. I&rsquo;ve used Rust for smaller personal projects in the past, but never created a full CLI tool. I am happy to report that the <code>clap</code> library for creating CLIs in Rust is great, and recommend it to anyone who might be interested.</p>
<p>The next piece that we needed was precompiled Rubies. There are a few big projects out there compiling Ruby in advance, mostly for use on servers. The <code>setup-ruby</code> GitHub action and the official Ruby docker images are both based on the <code>ruby-build</code> project originally started as part of <code>rbenv</code>.</p>
<p>Unfortunately, those existing precompiled Ruby versions aren&rsquo;t usable for our needs because they aren&rsquo;t <strong>statically compiled</strong> and because they aren&rsquo;t <strong>relocatable</strong>. Statically compiled (as opposed to dynamically compiled) means that Ruby copies the code from a shared library into its own binary.</p>
<p>Show of hands&hellip; have any of you ever had trouble compiling Ruby because of OpenSSL? Okay, put your hands down. Now, how many of you have had an already-installed Ruby suddenly stop working because of OpenSSL, and you had to install it again? Good news, <code>rv</code> fixes both of those problems by putting the OpenSSL inside the Ruby, so they can never get separated.</p>
<p>There is a tradeoff here—if there is a critical security flaw in OpenSSL, we will need to compile Ruby again to include the critical security update. The first reason we are okay with this tradeoff is that OpenSSL doesn&rsquo;t have huge security issues very often. The second reason we are okay with this is that your production servers are probably using the official Ruby docker images and not Ruby installed by <code>rv</code>, so it&rsquo;s even less of a concern.</p>
<p>In the end, the closest existing system we were able to build on top of was Homebrew&rsquo;s <code>portable-ruby</code> project. That&rsquo;s the system Homebrew uses to build the Ruby install that Homebrew itself runs on. The Homebrew team built some excellent infrastructure for building a statically linked Ruby, and even added the changes needed to make sure that Ruby could be relocated.</p>
<p>Since Homebrew needs to be able to install into <code>/usr/local</code> on x86, but <code>/opt/homebrew</code> on Apple Silicon, and into any user&rsquo;s home directory for Linuxbrew, they need to be able to take a single precompiled Ruby and put it in any location on disk. That&rsquo;s another one of the requirements that isn&rsquo;t met by the <code>setup-ruby</code> or Docker image Rubies—if you move them to another directory, they stop working.</p>
<p>Using Homebrew&rsquo;s <code>portable-ruby</code> as a base, we were able to start with macOS ARM and Ubuntu x86, add Ubuntu on ARM, and then build every version in the Ruby 3.4.x series. After a few weeks of work, we had some initial functionality working. <code>rv</code> could switch between installed Ruby versions in zsh, but most importantly <strong>it could install precompiled Ruby 3.4.x on macOS and Ubuntu in one second flat</strong>. Yes, you heard that right. <code>rv ruby install 3.4.5</code>. Wait 1 second. Done. You can run Ruby commands now.</p>
<p>With that proof of concept complete, we announced version 0.1. People got very excited! It was fantastic to see how many people were excited by the vision for a new, fast tool for Ruby development.</p>
<p>It&rsquo;s been a few weeks since that 0.1 release, and we have been hard at work. We&rsquo;ve expanded the team to include community contributors, merged pull requests, and compiled more Rubies than ever.</p>
<p>When 0.2 is released, in the very near future, it will include support for not just zsh, but bash, fish, and nushell. We have also added support for macOS on x86, meaning we now fully support x86 and ARM on both macOS and Linux. Finally, the precompiled Ruby versions available will expand to include all of Ruby 3.3 as well as 3.4. Just to top all of that off, every version of Ruby will have YJIT built in.</p>
<p>Our short-term plans include finishing support for Ruby 3.2, adding rubies that work with musl libc on Linux, and testing on more Linux distributions. Our longer-term plans including improving the way gems are compiled, so that installing your entire application and all of its gems can happen in just a few seconds.</p>
<p>We want to live in a future where anyone can run a Ruby command, or tool, or application in seconds (or less!). We&rsquo;re going to build that future, for ourselves and for everyone else.</p>
]]></content></entry></feed>