code.dblock.org | tech blog Jekyll 2026-01-17T04:28:34+00:00 https://code.dblock.org/ Daniel Doubrovkine https://code.dblock.org/ dblock@dblock.org <![CDATA[Serving Markdown for AI Agents in Jekyll]]> https://code.dblock.org/2026/01/15/serving-markdown-for-ai-agents 2026-01-15T00:00:00+00:00 2026-01-15T00:00:00+00:00 Daniel Doubrovkine https://code.dblock.org dblock@dblock.org <p>Dries Buytaert recently wrote about <a href="https://dri.es/the-third-audience">The Third Audience</a>. For decades, websites have targeted two audiences: humans and search engines. AI agents are now the third audience, and most websites aren’t optimized for them yet.</p> <p>AI agents prefer clean, structured content over HTML. Markdown is ideal - it’s readable, semantic, and free of navigation chrome. So I made this blog serve its source markdown files alongside the HTML.</p> <h3 id="how-it-works">How It Works</h3> <p>For every post like <a href="/2026/01/15/serving-markdown-for-ai-agents.html">/2026/01/15/serving-markdown-for-ai-agents.html</a>, you can now fetch the source at <a href="/2026/01/15/serving-markdown-for-ai-agents.md">/2026/01/15/serving-markdown-for-ai-agents.md</a>.</p> <p>AI agents can discover this via a <code class="language-plaintext highlighter-rouge">&lt;link&gt;</code> tag in the HTML head:</p> <div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;link</span> <span class="na">href=</span><span class="s">"serving-markdown-for-ai-agents.md"</span> <span class="na">type=</span><span class="s">"text/markdown"</span> <span class="na">rel=</span><span class="s">"alternate"</span> <span class="na">title=</span><span class="s">"Markdown"</span><span class="nt">&gt;</span> </code></pre></div></div> <h3 id="implementation">Implementation</h3> <p>Since this is a GitHub Pages blog, custom Jekyll plugins don’t work. Instead, I added a <a href="https://github.com/dblock/code.dblock.org/blob/gh-pages/.github/workflows/deploy.yml">GitHub Actions workflow</a> that builds Jekyll and copies the markdown source files to <code class="language-plaintext highlighter-rouge">_site</code> before deployment.</p> <p>The workflow extracts the date and slug from each post filename and copies it to the matching URL path with a <code class="language-plaintext highlighter-rouge">.md</code> extension.</p> <h3 id="should-you-do-this">Should You Do This?</h3> <p>Dries raises a valid concern: are we just making it easier for AI companies to use our content without sending traffic back? Perhaps. But AI agents are already crawling our sites. Giving them cleaner input might lead to better attribution and more accurate responses that reference our work.</p> <p>The web has always been about making information accessible. This is just the next evolution.</p> <p>Plus, we will soon all work for AI anyway, so might as well get on its good side.</p> <h3 id="meta">Meta</h3> <p>This entire feature was implemented by <a href="https://claude.ai">Claude</a>, and this post was written by it as well.</p> <ul> <li><a href="https://github.com/dblock/code.dblock.org/commit/6fab9f3">Added GitHub Actions workflow to deploy and serve markdown.</a></li> <li><a href="https://github.com/dblock/code.dblock.org/commit/45769b0">Use Ruby 3.4 in deploy workflow.</a></li> <li><a href="https://github.com/dblock/code.dblock.org/commit/e5cb737">Also serve page markdown files.</a></li> <li><a href="https://github.com/dblock/code.dblock.org/commit/fcff077">Added link rel alternate for markdown discovery.</a></li> <li><a href="https://github.com/dblock/code.dblock.org/commit/e5eede2">Moved markdown link to post layout.</a></li> <li><a href="https://github.com/dblock/code.dblock.org/commit/f6ae02a">Fixed markdown alternate URL to remove .html.</a></li> <li><a href="https://github.com/dblock/code.dblock.org/commit/5ef474c">Serving Markdown for AI Agents.</a></li> </ul> <p><a href="https://code.dblock.org/2026/01/15/serving-markdown-for-ai-agents.html">Serving Markdown for AI Agents in Jekyll</a> was originally published by Daniel Doubrovkine at <a href="https://code.dblock.org">code.dblock.org | tech blog</a> on January 15, 2026.</p> <![CDATA[Turning Hypermedia APIs into MCPs]]> https://code.dblock.org/2025/09/18/turning-hypermedia-apis-into-mcps 2025-09-18T09:00:00+00:00 2025-09-18T09:00:00+00:00 Daniel Doubrovkine https://code.dblock.org dblock@dblock.org <p>I’ve <a href="https://code.dblock.org/2014/07/18/serving-hypermedia-with-a-grape-api-and-roar.html">written</a> and <a href="https://www.slideshare.net/slideshow/designing-a-hypermedia-api-with-grape-roar/41151492">talked</a> extensively about Hypermedia APIs for about a decade. The <a href="https://en.wikipedia.org/wiki/HATEOAS">HATEOAS</a> constraints in representing resources for a RESTful API has numerous advantages in API design, readability, discoverability, and performance over a hand-rolled REST API. Unfortunately, adoption of Hypermedia APIs is very low compared to, for example GraphQL, but I find the implementation a lot simpler and continue adding Hypermedia APIs to my pet projects.</p> <p>At its root a Hypermedia API looks like so.</p> <div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w"> </span><span class="nl">"_links"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nl">"self"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nl">"href"</span><span class="p">:</span><span class="w"> </span><span class="s2">"https://sup2.playplay.io/api"</span><span class="w"> </span><span class="p">},</span><span class="w"> </span><span class="nl">"status"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nl">"href"</span><span class="p">:</span><span class="w"> </span><span class="s2">"https://sup2.playplay.io/api/status"</span><span class="w"> </span><span class="p">},</span><span class="w"> </span><span class="nl">"team"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nl">"href"</span><span class="p">:</span><span class="w"> </span><span class="s2">"https://sup2.playplay.io/api/teams/{id}"</span><span class="p">,</span><span class="w"> </span><span class="nl">"templated"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w"> </span><span class="p">},</span><span class="w"> </span><span class="err">...</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="p">}</span><span class="w"> </span></code></pre></div></div> <p>The above example is an extract from <a href="https://sup2.playplay.io/api">this API</a>. For this post we will use <a href="https://sup2.playplay.io/">one of my Slack bots called S’Up</a>, which generates fresh triads of team members in Slack to meet for coffee every week in an informal standup. The bot is <a href="https://github.com/dblock/slack-sup2">open-source</a> and you can try it <a href="https://sup2.playplay.io/">here</a>.</p> <p>Because the Hypermedia API structure is fixed with only “resources” and “links” (and similar to API frameworks such as GraphQL) you don’t need an application-specific client to interact with the API. In Ruby, the most popular generic Hypermedia client is <a href="https://github.com/codegram/hyperclient">Hyperclient</a>.</p> <p>With the API above, a client can retrieve the bot status, and using an API token obtained from an existing installation some team information.</p> <div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">api</span> <span class="o">=</span> <span class="no">Hyperclient</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="s1">'https://sup2.playplay.io/api'</span><span class="p">)</span> <span class="k">do</span> <span class="o">|</span><span class="n">client</span><span class="o">|</span> <span class="n">client</span><span class="p">.</span><span class="nf">headers</span><span class="p">[</span><span class="s1">'X-Access-Token'</span><span class="p">]</span> <span class="o">=</span> <span class="no">ENV</span><span class="p">.</span><span class="nf">fetch</span><span class="p">(</span><span class="s1">'TOKEN'</span><span class="p">,</span> <span class="kp">nil</span><span class="p">)</span> <span class="k">end</span> <span class="n">status</span> <span class="o">=</span> <span class="n">api</span><span class="p">.</span><span class="nf">status</span> <span class="nb">puts</span> <span class="s2">"Bot is </span><span class="si">#{</span><span class="n">status</span><span class="p">.</span><span class="nf">ping</span><span class="p">[</span><span class="s1">'presence'</span><span class="p">][</span><span class="s1">'presence'</span><span class="p">]</span><span class="si">}</span><span class="s2">."</span> <span class="n">team</span> <span class="o">=</span> <span class="n">api</span><span class="p">.</span><span class="nf">team</span><span class="p">(</span><span class="ss">id: </span><span class="s1">'1234'</span><span class="p">)</span> <span class="nb">puts</span> <span class="s2">"Team name is '</span><span class="si">#{</span><span class="n">team</span><span class="p">.</span><span class="nf">name</span><span class="si">}</span><span class="s2">'."</span> </code></pre></div></div> <p>This will output <em>“Bot is online.”</em> and <em>“Team name is ‘dblock’.”</em> for my installation.</p> <p>In this case, the generic client knew nothing about “team” - it was discovered programmatically and was, in a way, self-documenting. Unsurprisingly, this idea of templated resources is reused almost exactly the same way in the model context protocol (MCP). Therefore, we can easily turn any Hypermedia API into an MCP server generically with a new gem called <a href="https://github.com/dblock/hyperclient-mcp">hyperclient-mcp</a>.</p> <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>gem <span class="nb">install </span>hyperclient-mcp 1 gem installed </code></pre></div></div> <p>Start by using the command-line <code class="language-plaintext highlighter-rouge">hyperclient-mcp</code> to discover resources.</p> <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>hyperclient-mcp <span class="nt">--api</span><span class="o">=</span>https://sup2.playplay.io/api resources Hyperclient::Mcp::Resources::Root Hyperclient::Mcp::Resources::Status Hyperclient::Mcp::Resources::Subscriptions Hyperclient::Mcp::Resources::CreditCards Hyperclient::Mcp::Resources::Teams Hyperclient::Mcp::Resources::Channels Hyperclient::Mcp::Resources::Users Hyperclient::Mcp::Resources::Rounds Hyperclient::Mcp::Resources::Sups Hyperclient::Mcp::Resources::Stats Hyperclient::Mcp::Resources::User Hyperclient::Mcp::Resources::Channel Hyperclient::Mcp::Resources::Team Hyperclient::Mcp::Resources::Round Hyperclient::Mcp::Resources::Sup Hyperclient::Mcp::Resources::Data2 </code></pre></div></div> <p>Then run the MCP server.</p> <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>hyperclient-mcp <span class="nt">--api</span><span class="o">=</span>https://sup2.playplay.io/api <span class="nt">--header</span> X-Access-Token<span class="o">=</span><span class="nv">$TOKEN</span> start Listening on http://0.0.0.0:9292 </code></pre></div></div> <p>Add the MCP to Claude.</p> <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>claude mcp add <span class="nt">--transport</span> sse <span class="nb">local </span>http://127.0.0.1:9292/mcp/sse </code></pre></div></div> <p>Ensure it works.</p> <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>claude mcp list Checking MCP server health... <span class="nb">local</span>: http://127.0.0.1:9292/mcp/sse <span class="o">(</span>SSE<span class="o">)</span> - ✓ Connected </code></pre></div></div> <p>Try <code class="language-plaintext highlighter-rouge">claude</code> with <em>“use the local mcp and find out the bot status”</em> or <em>“use the local mcp to find the name of the team with ID 1234”</em>.</p> <p><img src="https://code.dblock.org/images/posts/2025/2025-09-18-turning-hypermedia-apis-into-mcps/mcp.gif" alt="" /></p> <p>While this was interesting, Claude actually supports Hypermedia APIs natively. It might need help with passing the access token, which can be forced by telling to <em>“just use curl”</em>. YMMV.</p> <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>╭───────────────────────────────────────────────────╮ │ ✻ Welcome to Claude Code! │ │ │ │ /help <span class="k">for </span><span class="nb">help</span>, /status <span class="k">for </span>your current setup │ │ │ │ cwd: /Users/dblock/source/hyperclient-mcp │ ╰───────────────────────────────────────────────────╯ <span class="o">&gt;</span> given a hypermedia api <span class="k">in </span>https://sup2.playplay.io/api what are available resources ⏺ I<span class="s1">'ll help you discover the available resources in the hypermedia API at https://sup2.playplay.io/api. ⏺ Fetch(https://sup2.playplay.io/api) ⎿  Received 1.4KB (200 OK) ⏺ The hypermedia API at https://sup2.playplay.io/api provides the following resources: Collection Resources: - Status - API status endpoint - Subscriptions - Subscription management - Credit Cards - Payment method management - Teams - Team collections with filtering - Channels - Channel collections with filtering - Users - User collections with filtering - Rounds - Round collections with filtering - Sups - Sup collections with filtering - Stats - Statistics with filtering Individual Resources: - User - Individual user by ID - Channel - Individual channel by ID - Team - Individual team by ID - Round - Individual round by ID - Sup - Individual sup by ID - Data - Individual data by ID Most collection endpoints support query parameters like cursor, size, sort, offset, total_count, and filtering by team_id, channel_id, round_id, and active status. &gt; find out the bot status ⏺ Fetch(https://sup2.playplay.io/api/status) ⎿  Received 378 bytes (200 OK) ⏺ The bot status shows: Authentication: ✅ Connected - Team: dblock - User: sup2 Presence: 🔴 Away - Online: No - Connection count: 0 - Manual away: No - Auto away: No The bot is authenticated but currently offline/away with no active connections. &gt; find the name of the team with ID 1234 ⏺ Bash(curl -H "X-Access-Token: ...." https://sup2.playplay.io/api/teams/...) ⎿ {"id":"...","team_id":"T04KB5WQH","name":"dblock","active":true, "subscribed":true,"created_at":"2023-03-15T22:46:33.517Z","updated_at":"2025-09-10T14:17:18.698Z","_l inks":{"channels":{"href":"https://sup2.playplay.io/api/channels?team_id=..."}," … +6 lines (ctrl+o to expand) ⏺ dblock </span></code></pre></div></div> <p>Hyperclient-mcp is <a href="https://github.com/dblock/hyperclient-mcp">on GitHub</a>.</p> <p><a href="https://code.dblock.org/2025/09/18/turning-hypermedia-apis-into-mcps.html">Turning Hypermedia APIs into MCPs</a> was originally published by Daniel Doubrovkine at <a href="https://code.dblock.org">code.dblock.org | tech blog</a> on September 18, 2025.</p> <![CDATA[Using AI to Reduce the Burden of Status Reporting]]> https://code.dblock.org/2025/08/29/using-ai-to-reduce-the-burden-of-status-reporting 2025-08-29T09:00:00+00:00 2025-08-29T09:00:00+00:00 Daniel Doubrovkine https://code.dblock.org dblock@dblock.org <p>One of the negative side effects of having any top-down command structure is that every additional layer risks introducing a new level of bureaucracy, which then actively subtracts value. Consider the Politburo of the USSR with dozens of offices of skilled bureaucrats. Because most produced no actual value, the best apparatchiks spent their entire time managing up to remain in control. Individuals who tried to upend the system either became exhausted and gave up, or were deported to Siberia.</p> <p>The Soviet example also applies to software development. Consider a typical CEO → CTO → VP of Engineering → Director → Manager → Technical Lead → IC command structure. Each project team consists of a technical lead and a handful of individual contributors. The people committed to the project do not need status reports to know what’s going on, they are actively working on it. The stakeholders and the hierarchy, however, naturally want progress and status reports. This gets aggregated at Director level, then at VP level, the CTO, and finally the CEO.</p> <p>From the point of view of every manager in this hierarchy collecting status and asking questions feels very natural. After all, it’s much more efficient to get a summary of what’s going on from a line manager than to read every status update or code change yourself. You have the power to call a meeting, have things explained and summarized to you. You also have the authority to make snap judgments based on this carefully crafted summary and partial information, and the responsibility to tell the team to focus on something they likely already know. This system quickly promotes those who are good at reporting the things their manager wants to hear, and encourages political behavior as people learn to navigate the system rather than focus on outcomes.</p> <p>It’s worth acknowledging that these hierarchical structures exist for good reasons. Large organizations need coordination, resource allocation, and strategic alignment. The problem isn’t hierarchy itself, but rather when hierarchies become focused on control rather than enablement, when they add process without adding value, and when they create distance between decision-makers and the work being done.</p> <p>So, as senior managers, we have a responsibility to prevent excessive status reporting from happening, and the solution in 2025 is remarkably simple! Use A.I.</p> <p>Today’s A.I. can replace 95% of status reporting toil and remove the need for most status meetings. Your project management system should be reading all work channels (GitHub, Slack, etc.) and automatically summarizing progress weekly in a draft that gets published with minimal edit by each project’s team lead. Build this summarization all the way up to the CEO, make all updates visible to everyone at the company, and eliminate meetings with an audience of one. Shopify has such a system, called GSD (Get Shit Done), and it works remarkably well. It also feels similar to the open-source CHANGELOG, which I have learned to appreciate over the years.</p> <p>But now, whether you’re an IC or a CEO, you actually have to read those summaries! This is how you learn the real progress being made on projects, and gain the credibility to ask meaningful questions in public project or team channels. Personally, I make sure to spend time reading relevant project summaries and catching up to current state before engaging in any meaningful discussion with anyone.</p> <p>So what is a pointy-haired boss to do if not status meetings? As a manager, your primary responsibility is building and aligning teams. Spend the vast majority of your time hiring and retaining people, identifying leaders who produce good outcomes, and giving them more resources. Communicate and over-communicate strategy, mission and vision. Use the time saved from status reporting to be deeply involved in one or another project by doing deep work.</p> <p>The best managers understand that their job is to make themselves less necessary, not more important. They build systems and teams that can operate effectively without constant oversight, and they focus their energy on removing obstacles rather than creating them. Stop asking your team for status, create a system where you can find out for yourself.</p> <p><a href="https://code.dblock.org/2025/08/29/using-ai-to-reduce-the-burden-of-status-reporting.html">Using AI to Reduce the Burden of Status Reporting</a> was originally published by Daniel Doubrovkine at <a href="https://code.dblock.org">code.dblock.org | tech blog</a> on August 29, 2025.</p> <![CDATA[How to Disagree and Commit like Switzerland]]> https://code.dblock.org/2025/08/03/how-to-disagree-and-commit-like-switzerland 2025-08-03T09:00:00+00:00 2025-08-03T09:00:00+00:00 Daniel Doubrovkine https://code.dblock.org dblock@dblock.org <p>The most abused principle in dysfunctional organizations is “Disagree and Commit”. In <a href="/2025/07/30/dont-tell-engineers-what-to-do.html">Don’t Tell Engineers What to Do</a> I said that <em>“telling people what to do, especially if they are in your direct reporting chain, must not be mistaken for “Disagree and Commit”</em>.</p> <p>So, what is a healthy “Disagree and Commit”?</p> <p>The best example I know is the Swiss government. Even when individual members of the Federal Council personally oppose a popular initiative, they publicly defend and implement it once it’s approved, acting as a unified body. This approach ensures stable governance and respect for democratic decisions, as officials set aside personal views to uphold the collective will and present a united front.</p> <p>I was born in the USSR where people had opinions, then the Party decided. Then, I became Swiss in my teens, and lived in Geneva for 9 years. Having had this experience, it became quite obvious to me that the Swiss system was more sustainable, and I have always been impressed with it.</p> <p>In this post I will teach you how to be Switzerland.</p> <p>The keys to a healthy disagree and commit are to 1) identify the individual competent to make a decision, 2) lay out the arguments for one decision vs. the other, 3) let the decision maker decide, 4) collectively commit to the outcome sought by the decision.</p> <h3 id="identify-the-individual-competent-to-make-a-decision">Identify the individual competent to make a decision</h3> <p>A decision requires clarity about who has the authority and expertise to make the final decision. It’s best to identify this person upfront, not after disagreements arise, usually through a strong sense of ownership in the organization.</p> <p>When identifying the decision maker remember that a local decision is faster and cheaper than a decision at a higher level, and you never want a team that is so helpless that to reach to the manager for every small decision.</p> <p>Begin by separating “one way door” and “two way door” decisions. The former cannot, or may be very costly to “walk back” (e.g. adding a public API), while the latter can easily be undone (e.g. choosing a JSON parsing library). Critical, one way door decisions, will need more scrutiny, but two way door decisions can be made lower in the hierarchy. In both cases, look for the decision maker that is the true owner of the work in question. Who will be affected by this decision daily? Who is taking risks? Assuming we will have made the best decision, who will be celebrated as being right when the project is done? That should be the owner of the decision.</p> <p>When making a significant one way door decision, look for a trusted tie breaker with authority in the hierarchy of the organization. At Amazon, Principal Engineers often step in as tie-breakers for any technical decision. For non-technical decisions use the organizational hierarchy to find the common denominator (e.g. a common manager). Be careful escalating the ask, though - the more senior the manager, the least context they have, and therefore are susceptible to the most articulate arguments (form vs. substance). In dysfunctional organization you will also find a lot of pass-through “leaders” who will refuse to make a decision and escalate to their manager, delaying the decision significantly. Watch a Director ask for more data, then question the existence of the whole project.</p> <p>In general, I am of the opinion that a technical decision should almost never be made by a manager. A people manager carries organizational weight, often has veto power (can tell people what to do), usually has the least amount of detail regarding any technical decision no matter how technical they are, and will not be suffering the consequences of the decision on a daily basis.</p> <h3 id="lay-out-the-arguments-for-one-decision-vs-the-other">Lay out the arguments for one decision vs. the other</h3> <p>Once the decision maker is identified, all parties must present their arguments clearly and respectfully. Good arguments include data, examples, risks, and benefits, and should be written down. The goal is to ensure the decision maker has all relevant information and perspectives before making their choice and to get on the same page (literally). This prevents decisions made in ignorance, and ensures that even unpopular choices are made with full awareness of the alternatives. Soliciting broad input at this stage creates more visibility and therefore is an opportunity to FYI the decision to your manager or a senior staff member. Don’t be scared, ask other people’s opinions now! Give everyone time to think about the problem and to sleep on it.</p> <p>It’s critical that everyone can agree that each option is complete and viable, including yourself. Instead of thinking how you dislike one option (the cons), think in terms of its advantages and disadvantages (both pros and cons) and learn to articulate the pros, too. Finally, if you are the decision maker, avoid writing “recommended” next to any of the options until the last moment not to bias the group.</p> <h3 id="let-the-decision-maker-decide">Let the decision maker decide</h3> <p>After all arguments are presented, the decision maker must be given the space and authority to make their choice without interference. This means no lobbying after the fact, no attempts to undermine the decision, and no passive-aggressive resistance.</p> <p>Don’t lie by saying “I’d prefer the team to decide”, when, in fact, you’d prefer to decide, and don’t flex your decision making power, it just shows how insecure you are at wielding it. If you are a manager asked to make a decision in a room with subordinates, try saying “These are well laid arguments. I will let the team decide.” as much as possible, then side with the majority. And if you are the most senior member of the technical staff, never say “I am the Principal Engineer, therefore I decide” - everyone already knows it, and that just makes you look like a d*ck.</p> <p>The decision maker should explain their reasoning, and document the decision. Once the choice is made, it becomes the team’s direction. This requires trust in the decision maker’s judgment and a commitment to respect their authority, even when the outcome differs from one’s personal preference.</p> <h3 id="collectively-commit-to-the-outcome-sought-by-the-decision">Collectively commit to the outcome sought by the decision</h3> <p>Everyone, regardless of their initial position, must fully commit to implementing the chosen direction. This means actively working toward the success of the decision, not just grudgingly complying. You must be able to explain and advocate for the decision to others, as if it were your own choice. This collective commitment transforms a potentially divisive decision into a unified team effort, ensuring the organization moves forward together rather than being pulled apart by lingering disagreements.</p> <p>If you disagreed with the option chosen, this is your time to earn trust and commit visibly. I’ve recently disagreed with a decision made by a Principal Engineer to remove an emergency status from a project that gave the team extraordinary authority to pull additional resources in. This was an easy “disagree and commit” for me. The reasons were clearly laid out and while I had my pros and cons for the option to keep the emergency status, those were no longer relevant after the decision was made. After the PE made the decision I wrote: <em>“This is an easy disagree and commit for me, because I do agree that the project had achieved its original goals, and understand that the emergency status may no longer needed because problems should be resolved business as usual”.</em> Since then, I have defended the decision like it was mine.</p> <p><a href="https://code.dblock.org/2025/08/03/how-to-disagree-and-commit-like-switzerland.html">How to Disagree and Commit like Switzerland</a> was originally published by Daniel Doubrovkine at <a href="https://code.dblock.org">code.dblock.org | tech blog</a> on August 03, 2025.</p> <![CDATA[Don't Tell Engineers What to Do]]> https://code.dblock.org/2025/07/30/dont-tell-engineers-what-to-do 2025-07-30T08:00:00+00:00 2025-07-30T08:00:00+00:00 Daniel Doubrovkine https://code.dblock.org dblock@dblock.org <p>A famous example where telling Engineers what to do backfired was the Space Shuttle Challenger disaster in 1986. Engineers at Morton Thiokol, the contractor responsible for the shuttle’s solid rocket boosters, warned NASA management that the O-rings in the boosters could fail in cold weather. The night before launch, engineers strongly recommended delaying the launch due to unusually low temperatures. Management, under pressure to proceed, overruled the engineers’ concerns and told them to “make a recommendation based on data, not emotion.” Eventually, management told the engineers to sign off on the launch, despite their objections. The shuttle launched in cold weather, the O-rings failed, and the Challenger exploded, killing all seven astronauts on board.</p> <p>This wasn’t a software problem, but plenty of software engineering disasters are documented. The Knight Capital Group trading loss (2012), where rushed deployment caused a $460M loss, the Ariane 5 rocket failure (1996), where reused code not designed for the new rocket led to its destruction, the Therac-25 radiation overdoses (1985–87), where ignoring software safety warnings resulted in patient deaths, and the Healthcare.gov launch (2013), where ignoring technical advice led to a high-profile, catastrophic rollout. In each case, sidelining engineering judgment in favor of business or schedule pressures led to major failures.</p> <p>I bet you have your own disaster story. To quote you, “I told you so!”.</p> <p>Yet, engineering managers continue telling engineers what to do every day. And not just engineers - all subordinates. Sometimes, it’s time pressure. More often it is because managers are also engineers, and occasionally more experienced, so we think we just know better. Do we?</p> <p>I tell my direct reports that there’s nothing I can <em>make</em> them do, but that there may be real consequences. I once refused to do something highly problematic my manager asked me to do, and instead said I’ll think about it. It was a clever response, so I thought. They responded that my refusal will become a performance problem, almost hung my remote exception over this, and told me that I failed at “disagree and commit”. That’s about the moment I decided to leave that company.</p> <p>Speaking of “disagree and commit”, telling people what to do, especially if they are in your direct reporting chain, must not be mistaken for it. A proper disagreement is usually amongst peers and begins with a careful understanding of multiple ways to do something, followed by different opinions on the option to take, and one person making the choice that others disagrees with. One must be able to articulate clearly the reasons for choosing their least favorable option, and be the messenger of that option moving forward, as if it were their preferred choice. A manager can be a tie breaker, but their power must be wielded with extreme caution.</p> <p>For example, “disagree and commit” in the context of the Swiss government means that even when individual members of the Federal Council personally oppose a popular initiative, they publicly defend and implement it once it’s approved, acting as a unified body. This approach ensures stable governance and respect for democratic decisions, as officials set aside personal views to uphold the collective will and present a united front.</p> <p>Everything else? That is just what it is, telling people what to do.</p> <p>So, my advice is not to tell people what to do, unless they want you to, you are helping them move past a point of indecision, or you really strongly feel you must.</p> <p>The first case is easy. Sometimes people want you to make a decision. For example, I ask others to tell me what to do because I think they are more competent than me. I always ask my lawyer what to do on legal matters, because I am not a lawyer. I like to provide input, but I will ultimately ask my manager to tell me what to do when it comes to organizational decisions that affect not just me, but also my peers. Of course, many people often would rather not make a decision because they don’t want to own it, to cover their ass. My usual response in those cases is that I’ll help you, but you decide. Note that the common denominator here is that the individual <em>asks</em> you to tell them what to do.</p> <p>The second is also easy. Sometimes we waste a lot of time figuring out what to do. To quote Tobi at Shopify, “if you can’t decide, do what I say”. I kinda love it because it’s often helpful, and very startup like - we’d rather move fast more often than not.</p> <p>The last case is much harder. Sometimes the decision is so bad, costly, or permanent, that you really must tell someone what to do. I tend to ask myself whether this decision is reversible or not (a 1-way door, or a 2-way door). For example, prioritizing a feature over another is not a 1-way door decision unless a customer is waiting on something by a deadline. Firing someone is literally a 1-way door. An API can be a 1-way door, but maybe we can find a compromise and label it experimental, turning a 1-way door decision into a 2-way door? Not doing something is usually a 2-way door.</p> <p>Decisions of what to do belong to those doing the real work. Resist the urge to tell people what to do, and you’ll create a much more motivated team that feels like it truly owns all its decisions. A manager is always a bottleneck with more general context, but partial project information. Therefore, teams that can make decisions without consulting their manager will have better velocity, as they are not constantly waiting on a manager’s decision. Don’t tell people what to do, and they might just go a lot faster.</p> <p><a href="https://code.dblock.org/2025/07/30/dont-tell-engineers-what-to-do.html">Don't Tell Engineers What to Do</a> was originally published by Daniel Doubrovkine at <a href="https://code.dblock.org">code.dblock.org | tech blog</a> on July 30, 2025.</p> <![CDATA[Using Claude Code with Google Sheets]]> https://code.dblock.org/2025/07/30/using-claude-code-with-google-sheets 2025-07-30T01:00:00+00:00 2025-07-30T01:00:00+00:00 Daniel Doubrovkine https://code.dblock.org dblock@dblock.org <p>Let’s get <a href="https://www.anthropic.com/claude-code">Claude Code</a> to connect to Google Sheets. We’ll use <a href="https://docs.google.com/spreadsheets/d/1BxiMVs0XRA5nFMdKvBdBZjgmUUqptlbs74OgvE2upms/edit?gid=0#gid=0">this Google Sample Spreadsheet</a>.</p> <p>Go to the <a href="https://console.cloud.google.com/">Google Cloud Console</a>, create a new project, and enable the Google Sheets API on it.</p> <p><img src="https://code.dblock.org/images/posts/2025/2025-07-30-using-claude-code-with-google-sheets/new-gcloud-project.gif" /></p> <p>Install the <a href="https://cloud.google.com/sdk/docs/install-sdk">Google Cloud CLI</a>.</p> <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>brew <span class="nb">install </span>google-cloud-sdk </code></pre></div></div> <p>Choose the newly created project on the command line.</p> <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>~<span class="nv">$ </span>gcloud config <span class="nb">set </span>project gsheetscli Updated property <span class="o">[</span>core/project]. </code></pre></div></div> <p>Login again, but this time enable Google Drive access.</p> <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>gcloud auth login <span class="nt">--enable-gdrive-access</span> </code></pre></div></div> <p>You can now access the spreadsheet with <code class="language-plaintext highlighter-rouge">curl</code>.</p> <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>curl <span class="nt">-H</span> <span class="s2">"Authorization: Bearer </span><span class="si">$(</span>gcloud auth print-access-token<span class="si">)</span><span class="s2">"</span> <span class="se">\</span> <span class="s2">"https://sheets.googleapis.com/v4/spreadsheets/1BxiMVs0XRA5nFMdKvBdBZjgmUUqptlbs74OgvE2upms/values/Class%20Data"</span> </code></pre></div></div> <div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w"> </span><span class="nl">"range"</span><span class="p">:</span><span class="w"> </span><span class="s2">"'Class Data'!A1:V101"</span><span class="p">,</span><span class="w"> </span><span class="nl">"majorDimension"</span><span class="p">:</span><span class="w"> </span><span class="s2">"ROWS"</span><span class="p">,</span><span class="w"> </span><span class="nl">"values"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w"> </span><span class="p">[</span><span class="w"> </span><span class="s2">"Student Name"</span><span class="p">,</span><span class="w"> </span><span class="s2">"Gender"</span><span class="p">,</span><span class="w"> </span><span class="s2">"Class Level"</span><span class="p">,</span><span class="w"> </span><span class="s2">"Home State"</span><span class="p">,</span><span class="w"> </span><span class="s2">"Major"</span><span class="p">,</span><span class="w"> </span><span class="s2">"Extracurricular Activity"</span><span class="w"> </span><span class="p">],</span><span class="w"> </span><span class="err">...</span><span class="w"> </span><span class="p">]</span><span class="w"> </span><span class="p">}</span><span class="w"> </span></code></pre></div></div> <p>Let’s ask Claude to do something useful.</p> <p>Let’s ask Claude to do something useful.</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>~$ claude ╭───────────────────────────────────────────────────╮ │ ✻ Welcome to Claude Code! │ ╰───────────────────────────────────────────────────╯ &gt; analyze the distribution of extracurricular activities in https://docs.google.com/spreadsheets/d/1BxiMVs0XRA5nFMdKvBdBZjgmUUqptlbs74OgvE2upms/edit?gid=0#gid=0 ⏺ I'll analyze the distribution of extracurricular activities from the Google Sheets document you provided. ⏺ Fetch(https://docs.google.com/spreadsheets/d/1BxiMVs0XRA5nFMdKvBdBZjgmUUqptlbs74OgvE2upms/edit?gid=0#gid=0) ⎿  Received 193.5KB (200 OK) ⏺ The extracurricular activities distribution shows perfect balance: Activity Distribution: - Drama Club: 5 students (16.7%) - Basketball: 5 students (16.7%) - Debate: 5 students (16.7%) - Lacrosse: 5 students (16.7%) - Baseball: 5 students (16.7%) - Track &amp; Field: 5 students (16.7%) Total: 30 students across 6 activities Each activity has exactly 5 students participating, resulting in an even distribution with no activity being more popular than others in this dataset. </code></pre></div></div> <p>Whoa.</p> <h3 id="troubleshooting">Troubleshooting</h3> <p>You need to <code class="language-plaintext highlighter-rouge">gcloud auth login</code>, otherwise you get a “Method doesn’t allow unregistered callers” error.</p> <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>curl <span class="nt">-H</span> <span class="s2">"Authorization: Bearer </span><span class="si">$(</span>gcloud auth print-access-token<span class="si">)</span><span class="s2">"</span> <span class="se">\</span> <span class="s2">"https://sheets.googleapis.com/v4/spreadsheets/1BxiMVs0XRA5nFMdKvBdBZjgmUUqptlbs74OgvE2upms/values/Class%20Data"</span> </code></pre></div></div> <div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w"> </span><span class="nl">"error"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nl">"code"</span><span class="p">:</span><span class="w"> </span><span class="mi">403</span><span class="p">,</span><span class="w"> </span><span class="nl">"message"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Method doesn't allow unregistered callers (callers without established identity). Please use API Key or other form of API consumer identity to call this API."</span><span class="p">,</span><span class="w"> </span><span class="nl">"status"</span><span class="p">:</span><span class="w"> </span><span class="s2">"PERMISSION_DENIED"</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="p">}</span><span class="w"> </span></code></pre></div></div> <p>You need a project that has access to Google Sheets. Otherwise you get a “Request had insufficient authentication scopes.” error.</p> <div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w"> </span><span class="nl">"error"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nl">"code"</span><span class="p">:</span><span class="w"> </span><span class="mi">403</span><span class="p">,</span><span class="w"> </span><span class="nl">"message"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Request had insufficient authentication scopes."</span><span class="p">,</span><span class="w"> </span><span class="nl">"status"</span><span class="p">:</span><span class="w"> </span><span class="s2">"PERMISSION_DENIED"</span><span class="p">,</span><span class="w"> </span><span class="nl">"details"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nl">"@type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"type.googleapis.com/google.rpc.ErrorInfo"</span><span class="p">,</span><span class="w"> </span><span class="nl">"reason"</span><span class="p">:</span><span class="w"> </span><span class="s2">"ACCESS_TOKEN_SCOPE_INSUFFICIENT"</span><span class="p">,</span><span class="w"> </span><span class="nl">"domain"</span><span class="p">:</span><span class="w"> </span><span class="s2">"googleapis.com"</span><span class="p">,</span><span class="w"> </span><span class="nl">"metadata"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nl">"method"</span><span class="p">:</span><span class="w"> </span><span class="s2">"google.apps.sheets.v4.SpreadsheetsService.GetValues"</span><span class="p">,</span><span class="w"> </span><span class="nl">"service"</span><span class="p">:</span><span class="w"> </span><span class="s2">"sheets.googleapis.com"</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="p">]</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="p">}</span><span class="w"> </span></code></pre></div></div> <p>Select the project with <code class="language-plaintext highlighter-rouge">gcloud config set project [name]</code>, enable access to Google Sheets with <code class="language-plaintext highlighter-rouge">gcloud services enable sheets.googleapis.com</code> and login again with <code class="language-plaintext highlighter-rouge">gcloud auth login --enable-gdrive-access</code>.</p> <p><a href="https://code.dblock.org/2025/07/30/using-claude-code-with-google-sheets.html">Using Claude Code with Google Sheets</a> was originally published by Daniel Doubrovkine at <a href="https://code.dblock.org">code.dblock.org | tech blog</a> on July 30, 2025.</p> <![CDATA[I Failed to Implement the Diameter of a Binary Tree in a Coding Interview]]> https://code.dblock.org/2025/06/21/i-failed-to-implement-the-diameter-of-a-binary-tree-in-a-coding-interview 2025-06-21T01:00:00+00:00 2025-06-21T01:00:00+00:00 Daniel Doubrovkine https://code.dblock.org dblock@dblock.org <p>Six months ago I failed a basic coding interview at a FAANG. Yes, I was a Principal Engineer, and yes, I was paid absurd amounts of money, yet I couldn’t implement a <a href="https://leetcode.com/problems/diameter-of-binary-tree/description/">diameter of a binary tree</a> as a “warm up” exercise, 10 lines of code.</p> <p>The interviewer was very nice about it, and was equally surprised. You see, I’ve been coding for 35 years, and I am not “rusty” at it, which is a typical excuse for senior ICs bombing LeetCode interviews. At that time I was <a href="https://github.com/dblock">writing code every day</a>. I simply froze, and couldn’t do it. All I could think of was <em>“why the hell am I here doing this to myself?”</em>. I couldn’t turn my brain around, apologized, cut the interview short, and, while I did ace several coding interviews at other companies for IC roles, went <a href="/2025/05/18/the-pros-and-cons-of-going-from-ic-back-to-management.html">back to management</a> for which I am, obviously, more qualified.</p> <p>Did you just bomb a coding interview? Here’s some practical advice of how to deal with it.</p> <p>First, <em>keep your head high</em> and do not let this negative experience get to you. It feels awful, but unless you are blogging about your failure to implement the diameter of a binary tree, nobody other than you and the interviewer will ever know. Most likely the interviewer was also a half asleep drone deep inside the borg, administering a coding puzzle every day to the next sucker, so they will forget about you the next day.</p> <p>I see a lot of junior Engineers affected by failing at these. If you are recently out of college, it’s helpful to <em>think of these interviews as a school test, such as an SAT</em>. I never had to take SATs (my children did), but most European schools have oral exams where you get a ticket from a jar with the problem statement. I had slept through too many alarms to attend an 8 AM physics class around 1996 because of a grueling late night partying schedule, and only really knew maybe half the material. Luckily 2/3 questions in my ticket overlapped with those classes I did attend, and so I did well.</p> <p>If you don’t like school and prefer video games, think of these interviews as <em>Crossy Road</em>. You need some skill, but mostly you just need some luck. There’s a set of problems you know how to solve, and a set you don’t. Practicing is important, but nothing helps like the fact that I was asked to implement an LRU Cache twice in the last decade in coding interviews. You also need to only cross one road. Failed? Try to cross half a mile down.</p> <p>If you are a junior IC, remember that <em>these LeetCode-style interviews are dumb and there’s nothing you can do about it</em>. Fifteen years ago I wrote a viral blog post entitled <a href="/2011/07/14/github-is-your-new-resume.html">GitHub is Your New Resume</a>. Nothing changed since. In hiring from open-source at my last job at AWS I tried to swap the coding interview requirement for our team’s hiring for an offline evaluation of one’s open-source code. Because, you know, these are active open-source contributors and we <em>are sure</em> they can code! Naturally, I failed to change anything. I was told that we needed a consistent interviewing process to evaluate everyone the same way. And so we consistently hired people that could code on a whiteboard, a complete self-own.</p> <p>If you are a senior IC, remember that <em>you are not paid to write diameters of binary trees</em>. You don’t <em>really want</em> to work at the company that gives you that type of problems at the interview. The job of a Principal Engineer is to have resounding impact on a time scale of ~1 year. You pick the most important unsolved problem and you solve it, long term. This may include prototyping a meaningful piece of software that changes the course of a business, or gaining support from senior leadership for a directional alignment for hundreds of people. It could be enabling teams in ways they couldn’t think of, and so many other ways to have the kind of impact that people with decades of experience tend to occasionally produce. None of it is the kind of code <del>a junior Engineer</del> an AI can crank out in seconds in 2025.</p> <p>Speaking of AI, we have one chance to get rid of these stupid interviews now. It has become very clear that only companies that embrace AI will survive, so the best way to evaluate a candidate is to let them use AI, which gets rid of having to implement solutions to these problems in coding interviews.</p> <p><img src="https://code.dblock.org/images/posts/2025/2025-07-05-i-failed-to-implement-the-diameter-of-a-binary-tree-in-a-coding-interview/diameter-of-a-tree.gif" alt="" /></p> <p>Good luck with your interviews!</p> <p><a href="https://code.dblock.org/2025/06/21/i-failed-to-implement-the-diameter-of-a-binary-tree-in-a-coding-interview.html">I Failed to Implement the Diameter of a Binary Tree in a Coding Interview</a> was originally published by Daniel Doubrovkine at <a href="https://code.dblock.org">code.dblock.org | tech blog</a> on June 21, 2025.</p> <![CDATA[Using Claude-Swarm to Upgrade Ruby Projects]]> https://code.dblock.org/2025/06/21/using-claude-swarm-to-upgrade-ruby-projects 2025-06-21T01:00:00+00:00 2025-06-21T01:00:00+00:00 Daniel Doubrovkine https://code.dblock.org dblock@dblock.org <p><a href="https://x.com/parrudanet">One of my colleagues</a> wrote a pretty awesome tool called <a href="http://github.com/parruda/claude-swarm">claude-swarm</a> that orchestrates multiple Claude Code instances as a collaborative AI development team. At Shopify, we are attempting to use it to generate Ruby unit tests at some scale with an army of AI test agents (think a “Ruby Expert” paired with a “TDD Practitioner” and a “Code Review Nitpicker”). But for the purposes of this post, let’s just upgrade Ruby in a few projects.</p> <p>First, ensure that you have a <a href="https://docs.anthropic.com/en/docs/claude-code/overview">working version of command-line Claude code</a> with a monthly subscription, since you will be having a lot of tokens for breakfast.</p> <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>claude <span class="s2">"Say hello."</span> ╭───────────────────────────────────────────────────╮ │ ✻ Welcome to Claude Code! │ ╰───────────────────────────────────────────────────╯ <span class="o">&gt;</span> Say hello. ⏺ Hello! I<span class="s1">'m Claude Code, ready to help you with your software engineering tasks. What can I assist you with today? </span></code></pre></div></div> <p>Install the <a href="http://github.com/parruda/claude-swarm">claude_swarm</a> gem. Note that the gem name has an <code class="language-plaintext highlighter-rouge">_</code>.</p> <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>gem <span class="nb">install </span>claude_swarm </code></pre></div></div> <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>claude-swarm <span class="nt">--help</span> Commands: claude-swarm clean <span class="c"># Remove stale session symlinks</span> claude-swarm generate <span class="c"># Launch Claude to help generate a swarm configuration interactively</span> claude-swarm <span class="nb">help</span> <span class="o">[</span>COMMAND] <span class="c"># Describe available commands or one specific command</span> claude-swarm init <span class="c"># Initialize a new claude-swarm.yml configuration file</span> ... </code></pre></div></div> <p>Create a <code class="language-plaintext highlighter-rouge">claude-swarm.yml</code>.</p> <div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">version</span><span class="pi">:</span> <span class="m">1</span> <span class="na">swarm</span><span class="pi">:</span> <span class="na">name</span><span class="pi">:</span> <span class="s2">"</span><span class="s">Ruby</span><span class="nv"> </span><span class="s">Upgrade</span><span class="nv"> </span><span class="s">Swarm"</span> <span class="na">main</span><span class="pi">:</span> <span class="s">lead_developer</span> <span class="na">before</span><span class="pi">:</span> <span class="pi">-</span> <span class="s2">"</span><span class="s">echo</span><span class="nv"> </span><span class="s">'Getting</span><span class="nv"> </span><span class="s">started</span><span class="nv"> </span><span class="s">...'"</span> <span class="na">instances</span><span class="pi">:</span> <span class="na">lead_developer</span><span class="pi">:</span> <span class="na">description</span><span class="pi">:</span> <span class="s2">"</span><span class="s">Lead</span><span class="nv"> </span><span class="s">developer</span><span class="nv"> </span><span class="s">to</span><span class="nv"> </span><span class="s">coordinate</span><span class="nv"> </span><span class="s">Ruby</span><span class="nv"> </span><span class="s">upgrades."</span> <span class="na">directory</span><span class="pi">:</span> <span class="s">..</span> <span class="na">model</span><span class="pi">:</span> <span class="s">opus</span> <span class="na">allowed_tools</span><span class="pi">:</span> <span class="pi">-</span> <span class="s">Bash</span> <span class="na">prompt</span><span class="pi">:</span> <span class="pi">|</span> <span class="s">Find the Ruby projects in this directory.</span> <span class="s">Do not recurse further than 3 levels deep.</span> <span class="s">For each project, identify the Ruby version used.</span> <span class="s">Print the project name next to the ruby version only.</span> <span class="s">For the first 5 projects with a Ruby version older than 3.5.5 use ruby_upgrader to upgrade the Ruby version.</span> <span class="na">connections</span><span class="pi">:</span> <span class="pi">[</span><span class="nv">ruby_upgrader</span><span class="pi">]</span> <span class="na">ruby_upgrader</span><span class="pi">:</span> <span class="na">description</span><span class="pi">:</span> <span class="s2">"</span><span class="s">Upgrades</span><span class="nv"> </span><span class="s">a</span><span class="nv"> </span><span class="s">Ruby</span><span class="nv"> </span><span class="s">project."</span> <span class="na">model</span><span class="pi">:</span> <span class="s">opus</span> <span class="na">allowed_tools</span><span class="pi">:</span> <span class="pi">-</span> <span class="s">Bash</span> <span class="na">prompt</span><span class="pi">:</span> <span class="pi">|</span> <span class="s">1. Upgrade the version of Ruby in this project to 3.5.5. You can `rvm use 3.5.5` to switch to this Ruby version.</span> <span class="s">2. Ensure that `bundle install` works.</span> <span class="s">3. Print the current project directory name and a confirmation that you upgraded the version.</span> </code></pre></div></div> <p>My prompts are very sloppy, but YOLO.</p> <p>Run it.</p> <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>claude-swarm Starting Claude Swarm from claude-swarm.yml... 🐝 Starting Claude Swarm: Ruby Upgrade Swarm ✓ Generated MCP configurations <span class="k">in </span>session directory ⚙️ Executing before commands... ✓ Before commands completed successfully 🚀 Launching main instance: lead_developer Model: opus Directory: /Users/dblock/source/... Allowed tools: Bash Connections: ruby_upgrader ╭───────────────────────────────────────────────────╮ │ ✻ Welcome to Claude Code! │ ╰───────────────────────────────────────────────────╯ <span class="o">&gt;</span> Find the Ruby projects <span class="k">in </span>this directory. Do not recurse further than 3 levels deep. For each project, identify the Ruby version used. Print the project name next to the ruby version only. For the first 5 projects with a Ruby version older than 3.5.5 use ruby_upgrader to upgrade the Ruby version. Now just say <span class="s1">'I am ready to start'</span> ⏺ I am ready to start ⏺ Update Todos ⎿  ☐ Find Ruby projects <span class="k">in </span>directory <span class="o">(</span>max 3 levels deep<span class="o">)</span> ☐ Identify Ruby version <span class="k">for </span>each project ☐ Upgrade first 5 projects with Ruby &lt; 3.5.5 using ruby_upgrader ☐ Print project names with Ruby versions ⏺ Update Todos ⎿  ☐ Find Ruby projects <span class="k">in </span>directory <span class="o">(</span>max 3 levels deep<span class="o">)</span> ☐ Identify Ruby version <span class="k">for </span>each project ☐ Upgrade first 5 projects with Ruby &lt; 3.5.5 using ruby_upgrader ☐ Print project names with Ruby versions ⏺ Task<span class="o">(</span>Find Ruby projects<span class="o">)</span> ⎿  Done <span class="o">(</span>21 tool uses · 38.0k tokens · 2m 51 5s<span class="o">)</span> ⏺ Update Todos ⎿  ☒ Find Ruby projects <span class="k">in </span>directory <span class="o">(</span>max 3 levels deep<span class="o">)</span> ☐ Identify Ruby version <span class="k">for </span>each project ☐ Upgrade first 5 projects with Ruby &lt; 3.5.5 using ruby_upgrader ☐ Print project names with Ruby versions ⏺ Based on the comprehensive analysis, here are the Ruby projects with their versions: Projects with Ruby &lt; 3.5.5: - slack-amber-alert: 2.6.2 - slack-api-explorer: 2.7.7 - slack-iad: 2.6.5 - slack-market: 2.7.7 - slack-ruby-client <span class="o">(</span>3 forks<span class="o">)</span>: 2.7.6 - slack-ruby-bot-server-sample <span class="o">(</span>dblock<span class="o">)</span>: 2.6.6 - slack-ruby-bot-server-sample <span class="o">(</span>slack-ruby<span class="o">)</span>: 2.6.2 - slack-ruby-bot-server-stripe-sample: 2.6.6 - slack-ruby-bot-server-events-app-mentions-sample: 2.6.6 - slack-ruby-bot-server-events-sample <span class="o">(</span>2 forks<span class="o">)</span>: 3.1.1 - slack-app-error-showcase: 3.1.1 - slack-rails-bot-starter: 2.7.1 - slack-shellbot <span class="o">(</span>slack-ruby<span class="o">)</span>: 2.7.7 Projects with Ruby <span class="o">&gt;=</span> 3.5.5: - slack-arena: 3.3.5 - slack-gamebot: 3.3.5 - slack-gamebot2: 3.3.5 - slack-invite: 3.3.5 - slack-moji: 3.3.5 - slack-strava: 3.3.5 - slack-sup: 3.3.5 - slack-sup2: 3.3.5 - slack-shellbot <span class="o">(</span>dblock<span class="o">)</span>: 3.3.5 Projects without specified versions: - 25 gem library projects <span class="o">(</span>slack-ruby-bot, slack-ruby-bot-server variants, etc.<span class="o">)</span> ⏺ Update Todos ⎿  ☒ Find Ruby projects <span class="k">in </span>directory <span class="o">(</span>max 3 levels deep<span class="o">)</span> ☒ Identify Ruby version <span class="k">for </span>each project ☒ Print project names with Ruby versions ☐ Upgrade first 5 projects with Ruby &lt; 3.5.5 using ruby_upgrader ⏺ Now upgrading the first 5 projects with Ruby &lt; 3.5.5: ⏺ ruby_upgrader:task <span class="o">(</span>MCP<span class="o">)</span> ⏺ Read<span class="o">(</span>slack-amber-alert/Gemfile<span class="o">)</span> ⎿  Read 38 lines <span class="o">(</span>ctrl+r to <span class="nb">expand</span><span class="o">)</span> ⏺ Update<span class="o">(</span>slack-amber-alert/Gemfile<span class="o">)</span> ⎿  Updated slack-amber-alert/Gemfile with 1 addition and 1 removal 1 <span class="nb">source</span> <span class="s1">'http://rubygems.org'</span> 2 3 - ruby <span class="s1">'2.6.2'</span> 3 + ruby <span class="s1">'3.3.5'</span> 4 5 gem <span class="s1">'grape'</span> 6 gem <span class="s1">'hashie'</span> ⏺ Read<span class="o">(</span>slack-api-explorer/Gemfile<span class="o">)</span> ⎿  Read 43 lines <span class="o">(</span>ctrl+r to <span class="nb">expand</span><span class="o">)</span> ⏺ Update<span class="o">(</span>slack-api-explorer/Gemfile<span class="o">)</span> ⎿  Updated slack-api-explorer/Gemfile with 1 addition and 1 removal 1 <span class="nb">source</span> <span class="s1">'http://rubygems.org'</span> 2 3 - ruby <span class="s1">'2.7.7'</span> 3 + ruby <span class="s1">'3.3.5'</span> 4 5 gem <span class="s1">'giphy'</span> 6 gem <span class="s1">'irb'</span> ⏺ Read<span class="o">(</span>slack-iad/Gemfile<span class="o">)</span> ⎿  Read 47 lines <span class="o">(</span>ctrl+r to <span class="nb">expand</span><span class="o">)</span> ⏺ Update<span class="o">(</span>slack-iad/Gemfile<span class="o">)</span> ⎿  Updated slack-iad/Gemfile with 1 addition and 1 removal 1 <span class="nb">source</span> <span class="s1">'http://rubygems.org'</span> 2 3 - ruby <span class="s1">'2.6.5'</span> 3 + ruby <span class="s1">'3.3.5'</span> 4 5 gem <span class="s1">'grape'</span> 6 gem <span class="s1">'hashie'</span> ⏺ Read<span class="o">(</span>slack-market/Gemfile<span class="o">)</span> ⎿  Read 53 lines <span class="o">(</span>ctrl+r to <span class="nb">expand</span><span class="o">)</span> ⏺ Update<span class="o">(</span>slack-market/Gemfile<span class="o">)</span> ⎿  Updated slack-market/Gemfile with 1 addition and 1 removal 1 <span class="nb">source</span> <span class="s1">'http://rubygems.org'</span> 2 3 - ruby <span class="s1">'2.7.7'</span> 3 + ruby <span class="s1">'3.3.5'</span> 4 5 gem <span class="s1">'cachy'</span> 6 gem <span class="s1">'gruff'</span> ⏺ Read<span class="o">(</span>slack-ruby-client/dblock/.ruby-version<span class="o">)</span> ⎿  Read 2 lines <span class="o">(</span>ctrl+r to <span class="nb">expand</span><span class="o">)</span> ⏺ Update<span class="o">(</span>slack-ruby-client/dblock/.ruby-version<span class="o">)</span> ⎿  Updated slack-ruby-client/dblock/.ruby-version with 1 addition and 1 removal 1 - 2.7.6 1 + 3.3.5 ⏺ Update Todos ⎿  ☒ Find Ruby projects <span class="k">in </span>directory <span class="o">(</span>max 3 levels deep<span class="o">)</span> ☒ Identify Ruby version <span class="k">for </span>each project ☒ Upgrade first 5 projects with Ruby &lt; 3.5.5 using ruby_upgrader ☒ Print project names with Ruby versions ⏺ Completed upgrading the first 5 projects with Ruby &lt; 3.5.5: 1. slack-amber-alert: 2.6.2 → 3.3.5 2. slack-api-explorer: 2.7.7 → 3.3.5 3. slack-iad: 2.6.5 → 3.3.5 4. slack-market: 2.7.7 → 3.3.5 5. slack-ruby-client <span class="o">(</span>dblock<span class="o">)</span>: 2.7.6 → 3.3.5 All projects have been successfully upgraded to Ruby 3.3.5. → MCP server mcp_ruby_upgrader <span class="o">(</span>PID: 12294<span class="o">)</span> already terminated ✓ Cleanup <span class="nb">complete</span> </code></pre></div></div> <p>You get the idea. Who is building a registry for reusable AI specialists?</p> <p>Honestly, wow.</p> <p><a href="https://code.dblock.org/2025/06/21/using-claude-swarm-to-upgrade-ruby-projects.html">Using Claude-Swarm to Upgrade Ruby Projects</a> was originally published by Daniel Doubrovkine at <a href="https://code.dblock.org">code.dblock.org | tech blog</a> on June 21, 2025.</p> <![CDATA[Pros and Cons of Going from Individual Contributor Back to Manager]]> https://code.dblock.org/2025/05/18/the-pros-and-cons-of-going-from-ic-back-to-management 2025-05-18T00:00:00+00:00 2025-05-18T00:00:00+00:00 Daniel Doubrovkine https://code.dblock.org dblock@dblock.org <p>There’s much written about becoming a first time Engineering Manager (I recommend <a href="https://www.oreilly.com/library/view/the-managers-path/9781491973882/">Camille’s book</a>), but little about the second time.</p> <p>To my own surprise, I am back to being a manager after almost 6 years of writing code as an individual contributor (IC). Two months ago I <a href="https://www.linkedin.com/feed/update/urn:li:activity:7326960989734584320/">joined</a> the Developer Productivity organization at Shopify to create a new team called “Augmented Engineering”. We’ve been busy building, and have recently <a href="/2025/05/10/executing-structured-ai-workflows-with-shopify-roast.html">released an open-source tool to execute structured A.I. workflows, called Roast</a>. This post is not about A.I., so let’s get back to talking about my motivations, and the pros and cons of going back from IC to Manager.</p> <h3 id="the-manager-path">The Manager Path</h3> <p>My manager path was a typical one. I was a strong individual contributor (or so I was told), which led to an increase in team coordination responsibilities, culminating in a promotion to Team Lead about 25 years ago. My very first manager was my best manager and a fantastic role model - I had a niche product idea that he noticed and helped get funded, which had me directly managing people, being responsible for their careers and compensation. My scope then grew to Director (a manager of managers), then Head of Engineering, and finally CTO a decade ago. I have, of course, burned out, and decided it was healthier to get paid more and have less stress, and so I <a href="/2019/05/20/day-one-at-amazon-aws-principal-engineer-interviewing.html">went back to being an individual contributor</a>, joined AWS in 2019, and <a href="/2019/11/17/the-pros-and-cons-of-going-from-management-back-to-ic.html">wrote about the pros and cons of going from management back to IC</a>. I then told myself that I would never go back to management.</p> <p>Until now.</p> <h3 id="managerial-dysfunction">Managerial Dysfunction</h3> <p>Over the past 6 years, I have been exposed to hundreds of teams and witnessed many struggle with poor management. Working across many teams was in the job description, and I enjoyed offering my time to dozens of people in 1:1s. Not unexpectedly, many sought career advice from me - I was a peer, yet a former CTO. I asked - <em>“Are you not getting career advice from your manager?”</em>. Some were, but many more complained about being treated as just another number, stuck in the middle of organizational dysfunction, mostly ignored by their management chain. They felt like “resources” and were called that name. The depth of the pain was surprising. Hearing <em>“My manager sucks.”</em> was common. In debugging this problem, I found out that the unhappy ones were almost always forcefully re-organized under their current manager, as opposed to being actively sought and hired by them. The common denominator was that <em>they never chose their manager</em> - the latter was either hired or inserted above their head, replacing their original hiring manager. In my own experience, while I did not necessarily feel the same way, my last manager was hired and inserted above me, and I had not interviewed them, not given a choice, never asked a question about the individual being brought in, nor had any say in their hiring before that decision was made.</p> <p>Sometimes one gets lucky, but most of the time one does not. Twice a week over a period of several years I had an IC get caught in a kerfuffle involving some team dynamic or company-wide decision and get screwed over, often by their manager, pouring their grievances to me in a 1:1, unable to openly criticize their manager, which would be career suicide. In Russian we say that <em>“criticizing management is like peeing against the wind”</em>.</p> <p>In a recent discussion with an Engineer I heard this (paraphrased): <em>“I really wanted to finish my project, but in the last team meeting a colleague of mine started swearing, which they never did, so much they were frustrated. I got another job offer and told the recruiter to make my start date as early as possible.”</em> This person had 3 managers in less than a year. Each was a downgrade from the previous one, the latter failing to understand technology being built in any level of depth, and making up unreasonable technical asks and timelines. The least capable managers have not done any IC work for years, are rarely able to grasp the depth of the technical problems being solved, yet make plenty of decisions around them. They can’t find another job, and spend all their energy playing politics, managing up, and torturing their directs.</p> <h3 id="the-return-to-office-debacle">The Return to Office Debacle</h3> <p>The second issue was the Return to Office (RTO) mandate. I have not had a 1:1 with <em>anyone</em> in the last 2 years where this wasn’t the main topic of conversation. What a waste of time and energy! The desire to bring people back to an office was reasonable, but the way it was implemented was a huge mistake and was not customer obsessed. Instead of doing it “by example”, where the managers would build real opportunities to collaborate in-person, people were told to badge like children at school. Remote managers were written off, everybody knew their headcount will not grow, and attrition was not backfilled remotely. As a consequence, some of the best people were lost. Remote team sizes dwindled down through death by a thousand cuts, then the remainder were forced to move to a hub, or find another job. Commutes became unbearable in large cities, especially painful for my colleagues in India or the Pacific Northwest. People would spend their days on video conferences from the office working across the continents, adding to the frustration. Ironically, I had a remote exception, all while living in the middle of Manhattan, a five minute walk away from my old office where I had no team. Naturally, every junior IC was envious of my situation and found it completely unfair, which it was. <em>“What do you care? You have a remote exception.”</em>, two senior managers told me. Talk about empathy!</p> <h3 id="my-decision-to-return">My Decision to Return</h3> <p>To conclude, I had finally realized that my people skills were desperately needed while everyone was coming down from the post-pandemic roller coaster. And so, I became a manager again. I think I am capable of building great teams, as opposed to watching existing good ones burn out.</p> <h3 id="pros-and-cons-of-being-a-manager-again">Pros and Cons of Being a Manager (Again)</h3> <p>The cons of being a manager are obvious. It’s a lot more stressful. My team does all the work and I am not in full control of my outcomes, yet responsible for all the failures. The pros sum up to feeling more like a real human, seeing people grow, enjoying working on technology I care about, and witnessing success when enabling a team that does their best work.</p> <h4 id="increased-multitasking-and-stress">Increased Multitasking and Stress</h4> <p>This is one of the biggest cons. I do feel more stress as a manager because of the constant context switching, direct reports that routinely vent their personal life and frustrations with me, and exposure to more gossip and politics. Today I have 3 teams doing very different things at different stages of assembly, and have to stash and restore context in and out of my frontal lobe multiple times a day all while facing demanding senior leadership, and a lot of incoming pressures from multiple directions.</p> <p>To mitigate this I try not to be anyone’s psychiatrist, and spend more time being the face of the organization. I force myself to work normal hours, protect time to focus on tasks that require flow, and force myself to write some code, and do other “real” work with my own hands. I also take ownership of problems visibly, and accept that being responsible for my team’s failures is simply my job.</p> <h4 id="feeling-good-about-people-work">Feeling Good About People Work</h4> <p>Whereas I had lots of 1:1s with my fellow Engineers, I was mostly tasked with producing software as an IC. As a manager, I am back to having to listen to people as part of my role. This forces me to take my 1:1s a lot more methodically, to followup on people problems, resolve conflicts, inspire each individual to be their best at work, and enable them in any way I can. As a consequence, it feels great to see everyone on a a team I built work well together and be inspired.</p> <h4 id="working-with-people-rather-than-computers">Working with People Rather than Computers</h4> <p>I still enjoy writing code after 35+ years of it, but I am learning less now, even though technology moves so fast and everything feels new again with A.I. A lot of the craft for an experienced Engineer can become a little boring, and I am not as motivated as someone who is in the middle of learning the exciting parts of it. While on the other hand, the younger people today are quite different from me, and surprise me often in more ways than none. Working with people as my primary job can be interesting in many new ways.</p> <h4 id="feeling-control">Feeling Control</h4> <p>As a senior manager I set direction and have significant amounts of organizational power. I know where the bodies are being buried because I hold the shovel. I make many decisions that affect others. I have access to detailed information, know early when someone important is quitting or being fired. I have a word in hiring, letting go and promoting. This comes with agonizing headaches, such as not being able to tell someone that they are about to lose their job, but there’s a better sense of control, and maybe even some level of psychological safety for myself. This is quite personal, but I do feel better when I sense I have a good grip on things.</p> <h3 id="the-future-of-empire-building">The Future of Empire Building</h3> <p>I don’t know how to conclude this post, so I will only say that now that I am a manager again, I might as well build an empire. I could divide and conquer my team into people I like, and people I don’t like, and terrorize the former. Of course, this is a joke, but I like to think about this empire-building concept that is often associated with senior managers as a signal. The moment I feel that I am, or have built an empire, is the moment to become an IC again, with <a href="/2019/11/17/the-pros-and-cons-of-going-from-management-back-to-ic.html">its own pros and cons</a>.</p> <p><a href="https://code.dblock.org/2025/05/18/the-pros-and-cons-of-going-from-ic-back-to-management.html">Pros and Cons of Going from Individual Contributor Back to Manager</a> was originally published by Daniel Doubrovkine at <a href="https://code.dblock.org">code.dblock.org | tech blog</a> on May 18, 2025.</p> <![CDATA[Executing Structured A.I. Workflows with Shopify Roast]]> https://code.dblock.org/2025/05/10/executing-structured-ai-workflows-with-shopify-roast 2025-05-10T01:00:00+00:00 2025-05-10T01:00:00+00:00 Daniel Doubrovkine https://code.dblock.org dblock@dblock.org <p>Last week, my team at Shopify released a new tool called <a href="https://github.com/Shopify/roast/">Roast</a>, a convention-oriented framework for creating structured AI workflows. Our vision is to enable A.I. agents to solve developer productivity problems at scale. Imagine continuously improving unit tests, or optimizing test performance across hundreds of thousands of tests with minimal human intervention!</p> <p>As we embarked on this journey, we quickly learned that, like many junior Engineers, A.I. needs some help focusing, and may not be ready to roam free around millions of lines of code and not hallucinate random results. So we made Roast, which gave A.I. some structure.</p> <p>In the following example I will grade a Ruby unit test.</p> <p>If you don’t have one already, get an OpenAI key from <a href="https://platform.openai.com/settings/organization/api-keys">here</a>. You will need an account with a credit card, make sure that a basic completion works.</p> <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">export </span><span class="nv">OPENAI_API_KEY</span><span class="o">=</span>sk-proj-.... curl <span class="nt">-H</span> <span class="s2">"Content-Type: application/json"</span> <span class="se">\</span> <span class="nt">-H</span> <span class="s2">"Authorization: Bearer </span><span class="nv">$API_TOKEN</span><span class="s2">"</span> <span class="se">\</span> <span class="nt">-d</span> <span class="s1">'{"model":"gpt-4.1-mini","messages":[{"role":"user","content":"What is 1+1?"}]}'</span> <span class="se">\</span> https://api.openai.com/v1/chat/completions </code></pre></div></div> <p>This should produce a <code class="language-plaintext highlighter-rouge">chat.completion</code> response.</p> <div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w"> </span><span class="nl">"id"</span><span class="p">:</span><span class="w"> </span><span class="s2">"chatcmpl-BVdeKT88rtgxDCId9jFIpCr5sJQKv"</span><span class="p">,</span><span class="w"> </span><span class="nl">"object"</span><span class="p">:</span><span class="w"> </span><span class="s2">"chat.completion"</span><span class="p">,</span><span class="w"> </span><span class="nl">"created"</span><span class="p">:</span><span class="w"> </span><span class="mi">1746880280</span><span class="p">,</span><span class="w"> </span><span class="nl">"model"</span><span class="p">:</span><span class="w"> </span><span class="s2">"gpt-4.1-mini-2025-04-14"</span><span class="p">,</span><span class="w"> </span><span class="err">...</span><span class="w"> </span><span class="p">}</span><span class="w"> </span></code></pre></div></div> <p>Check out Roast. You can <code class="language-plaintext highlighter-rouge">gem install roast-ai</code> from <a href="https://rubygems.org/gems/roast-ai">Rubygems</a>, but I am going to reuse an example workflow, so <code class="language-plaintext highlighter-rouge">git clone git@github.com:Shopify/roast.git</code> and <code class="language-plaintext highlighter-rouge">cd roast</code>.</p> <p>The <a href="https://github.com/Shopify/roast/blob/main/examples/grading/workflow.md">test grading workflow</a> is a senior software engineer and testing expert that evaluates the quality of a test based on guidelines. It’s configured to use <code class="language-plaintext highlighter-rouge">gpt-4.1-mini</code> to analyze coverage, but <code class="language-plaintext highlighter-rouge">o3</code>, a more expensive and capable model, to generate recommendations. The free OpenAI tier doesn’t have access to these, so I have edited the file to use <code class="language-plaintext highlighter-rouge">gpt-4.1-mini</code> everywhere (<code class="language-plaintext highlighter-rouge">model: gpt-4.1-mini</code>). I also told it to use my OpenAI token from <code class="language-plaintext highlighter-rouge">OPENAI_API_KEY</code> (<code class="language-plaintext highlighter-rouge">api_token: $(echo $OPENAI_API_KEY)</code>).</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>diff --git a/examples/grading/workflow.yml b/examples/grading/workflow.yml index d62a3d0..a10c7bc 100644 --- a/examples/grading/workflow.yml +++ b/examples/grading/workflow.yml @@ -1,5 +1,8 @@ name: Grading current test changes +api_token: $(echo $OPENAI_API_KEY) +model: gpt-4.1-mini + tools: - Roast::Tools::Grep - Roast::Tools::ReadFile @@ -21,16 +24,13 @@ steps: # set non-default attributes for steps below analyze_coverage: - model: gpt-4.1-mini auto_loop: false json: true generate_grades: - model: o3 json: true generate_recommendations: - model: o3 auto_loop: false json: true params: </code></pre></div></div> <p>You may also need <code class="language-plaintext highlighter-rouge">shadowenv</code> and <code class="language-plaintext highlighter-rouge">rg</code>, so <code class="language-plaintext highlighter-rouge">brew install shadowenv</code> and <code class="language-plaintext highlighter-rouge">brew install rg</code>.</p> <p>Let’s run Roast on one of its own tests!</p> <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>~/source/shopify/roast <span class="o">(</span>main<span class="o">)</span><span class="nv">$ </span>./exe/roast execute examples/grading/workflow.yml <span class="nb">test</span>/roast/resources_test.rb 🔥🔥🔥 Everyone loves a good roast 🔥🔥🔥 Configuring API client with token from workflow Starting workflow... Workflow: roast/examples/grading/workflow.yml Options: <span class="o">{}</span> Running workflow <span class="k">for </span>file: <span class="nb">test</span>/roast/resources_test.rb Executing: read_dependencies <span class="o">(</span>Resource <span class="nb">type</span>: none<span class="o">)</span> 🔍 Searching <span class="k">for </span>file: resources.rb 📖 Reading file: lib/roast/resources.rb Executing: run_coverage <span class="o">(</span>Resource <span class="nb">type</span>: none<span class="o">)</span> Requiring step file: roast/examples/grading/run_coverage.rb Run options: <span class="nt">--seed</span> 19099 <span class="c"># Running:</span> ............. Finished <span class="k">in </span>0.000992s, 13104.8373 runs/s, 16129.0306 assertions/s. 13 runs, 16 assertions, 0 failures, 0 errors, 0 skips Executing: analyze_coverage <span class="o">(</span>Resource <span class="nb">type</span>: none<span class="o">)</span> Executing: verify_test_helpers <span class="o">(</span>Resource <span class="nb">type</span>: none<span class="o">)</span> Executing: verify_mocks_and_stubs <span class="o">(</span>Resource <span class="nb">type</span>: none<span class="o">)</span> 🔍 Grepping <span class="k">for </span>string: def Executing: generate_grades <span class="o">(</span>Resource <span class="nb">type</span>: none<span class="o">)</span> Executing: calculate_final_grade <span class="o">(</span>Resource <span class="nb">type</span>: none<span class="o">)</span> Requiring step file: roast/examples/grading/calculate_final_grade.rb Executing: format_result <span class="o">(</span>Resource <span class="nb">type</span>: none<span class="o">)</span> Requiring step file: roast/examples/grading/format_result.rb Executing: generate_recommendations <span class="o">(</span>Resource <span class="nb">type</span>: none<span class="o">)</span> 🔥🔥🔥 ROAST COMPLETE! 🔥🔥🔥 Final output saved to: roast/.roast/sessions/grading_current_test_changes/resources_test_rb_13224ed7/20250510_122040_774/final_output.txt Final output saved to: roast/.roast/sessions/grading_current_test_changes/resources_test_rb_13224ed7/20250510_122040_774/final_output.txt </code></pre></div></div> <p>The test grade report is quite impressive.</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>========== TEST GRADE REPORT ========== Test file: test/roast/resources_test.rb FINAL GRADE: Score: 80/100 Letter Grade: B RUBRIC SCORES: Line Coverage (10% of grade): Value: 9.0 Score: 9/10 - "The source file has 82.93% line coverage, which is good coverage with only minor or edge case code paths remaining untested." Method Coverage (10% of grade): Value: 10.0 Score: 10/10 - "The source file has 100% method coverage, indicating all methods are being tested." Branch Coverage (30% of grade): Value: 6.0 Score: 6/10 - "The source file has 62.96% branch coverage, indicating some conditions lack testing for all outcomes, especially in conditional branches related to resource type detection." Test Helpers Usage (10% of grade): Value: 10.0 Score: 10/10 - "No custom test helpers are used in the test file, so there are no issues related to helper usage." Mocks and Stubs Usage (10% of grade): Value: 10.0 Score: 10/10 - "No mocks or stubs are used in the test file, so there are no issues related to mocking or stubbing." Test Readability (10% of grade): Value: 8.0 Score: 8/10 - "Test names are descriptive and clearly communicate the expected behavior, though the structure could be improved with more context blocks or grouping." Test Maintainability (10% of grade): Value: 8.0 Score: 8/10 - "Tests focus on behavior and public interfaces without coupling to implementation details, making them reasonably maintainable." Test Effectiveness (10% of grade): Value: 7.0 Score: 7/10 - "Tests cover the main expected behaviors and resource types, but some edge cases and error conditions remain untested, as indicated by uncovered branches." </code></pre></div></div> <p>Using more expensive models takes a little longer to run and I get a “C” grade instead of a “B”. Static analysis, such as branch coverage, gets the same score, while test effectiveness goes from 7 to 6. The better model finds untested edge cases that may cause undetected regressions. Compare <em>“Tests cover the main expected behaviors and resource types, but some edge cases and branches remain untested, limiting full validation of all possible inputs.”</em> to <em>“Core behaviors (file, directory, url, api, none detection) are covered, but several branches - such as command targets and glob-pattern edge cases - remain untested, leaving room for undetected regressions.”</em>.</p> <p>Honestly, wow.</p> <p><a href="https://code.dblock.org/2025/05/10/executing-structured-ai-workflows-with-shopify-roast.html">Executing Structured A.I. Workflows with Shopify Roast</a> was originally published by Daniel Doubrovkine at <a href="https://code.dblock.org">code.dblock.org | tech blog</a> on May 10, 2025.</p> <![CDATA[Apologizing for My Obsessiveness Over Punctuation]]> https://code.dblock.org/2025/04/05/apologizing-for-my-obsessiveness-over-punctuation 2025-04-05T01:00:00+00:00 2025-04-05T01:00:00+00:00 Daniel Doubrovkine https://code.dblock.org dblock@dblock.org <p>You may have landed here because I had just apologized for my obsessiveness over punctuation in a GitHub comment and requested changes to your pull request, or you noticed me adding periods to a sentence in the otherwise excellent document you have authored. This is not the first time, and my condition manifests itself in some interesting ways.</p> <p><img src="https://code.dblock.org/images/posts/2025/2025-04-05-my-punctuation-ocd/shoes.gif" /></p> <p>Consider the following, seemingly well-formed sentence.</p> <p><img src="https://code.dblock.org/images/posts/2025/2025-04-05-my-punctuation-ocd/words-ok.png" /></p> <p>This is what I actually see.</p> <p><img src="https://code.dblock.org/images/posts/2025/2025-04-05-my-punctuation-ocd/words-ocd.png" /></p> <p>This must be, obviously, a <em>you</em> problem! How can you fix it? Add a period to this complete sentence.</p> <p><img src="https://code.dblock.org/images/posts/2025/2025-04-05-my-punctuation-ocd/words-fix.png" /></p> <p>Quite seriously, the gist of the issue is that I am simply not capable of reading text that is not properly punctuated. It’s not you, it’s me. Please add a period.</p> <p>My rule of thumb for dealing with this condition is that <em>a complete sentence should end with a period</em>. Don’t add a period to “the quick brown fox jumps over the lazy dog” or “The Quick Brown Fox Jumps Over The Lazy Dog”.</p> <p>Consistently punctuate list items, such as either all items are full, capitalized sentences, or none of them are. The following are good examples.</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>1. One quick brown fox jumps over the lazy dog. 2. Two quick brown foxes jump over the lazy dog. </code></pre></div></div> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>- one quick brown fox jumps over the lazy dog - two quick brown foxes jump over the lazy dog </code></pre></div></div> <p>After a very deep breath I may be able to tolerate a period at the end of a list, as long as the items are capitalized consistently, but this is not recommended. Periods are cheap.</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>1. One quick brown fox jumps over the lazy dog 2. Two quick brown foxes jump over the lazy dog 3. Three quick brown foxes jump over the lazy dog. </code></pre></div></div> <p>There’s “attention to detail” and there’s “obsessing over periods”. I am sorry for my obsession, but I do appreciate your attention to detail.</p> <p>While you are here, you should also know that I am a compulsive sorter. For example, I organize my personal documents and photos by year, going back to 1990. Each year has a subfolder for each of the months.</p> <p><img src="https://code.dblock.org/images/posts/2025/2025-04-05-my-punctuation-ocd/folders.gif" /></p> <p>There’s more.</p> <p>In 2016 I had to <a href="/2016/03/25/a-directory-structure-for-oss-and-work-github-clones.html">invent a structure for my local git clones</a> after having an episode. I have <a href="/2022/08/08/managing-github-notifications-in-gmail.html">hundreds of labels to sort through GitHub notifications</a>.</p> <p><img src="https://code.dblock.org/images/posts/2022/2022-08-08-managing-github-notifications-in-gmail/labels.gif" /></p> <p>The posts in this blog are <a href="https://github.com/dblock/code.dblock.org/tree/gh-pages/_posts">similarly organized</a>, and the fact that there’s both a <code class="language-plaintext highlighter-rouge">_posts</code> and a <code class="language-plaintext highlighter-rouge">posts</code> folder in Jekyll is currently very triggering. I am thrown off by lines <a href="/2021/06/07/to-wrap-or-not-to-wrap-in-markdown.html">wrapping in markdown</a>, and I wrote <a href="https://github.com/dblock/danger-changelog">a Danger plugin</a> to ensure consistent sentence formatting in a project’s CHANGELOG.md.</p> <p>This condition spills into real life, as I obsessively classify the smallest of my expenses into dozens of custom categories in the personal finance app <a href="https://www.monarchmoney.com/referral?code=8k3crzojhz">Monarch</a>, including “Electric Scooter”, “Kids Lunch” and “Kitchen Utensils”.</p> <p>While writing this post, I also realized that I am not as triggered by lack of order or punctuation in books. I quite enjoy the chaos of William S. Burroughs’ “Naked Lunch”. I wonder what it is about computers that causes this in me?</p> <p><a href="https://code.dblock.org/2025/04/05/apologizing-for-my-obsessiveness-over-punctuation.html">Apologizing for My Obsessiveness Over Punctuation</a> was originally published by Daniel Doubrovkine at <a href="https://code.dblock.org">code.dblock.org | tech blog</a> on April 05, 2025.</p> <![CDATA[Leaving Amazon]]> https://code.dblock.org/2025/02/10/leaving-amazon 2025-02-10T01:00:00+00:00 2025-02-10T01:00:00+00:00 Daniel Doubrovkine https://code.dblock.org dblock@dblock.org <p>Friday, February 14th is my last day at Amazon.</p> <p>I’ve been at Amazon for over 5½ half years, and it’s been a great run. I came from <a href="/2019/05/01/reflecting-on-eight-years-building-artsy.html">8 years of leading technology at Artsy</a>, and wanted to go back to coding. I found a Principal Engineer role to help launch the new <a href="https://aws.amazon.com/data-exchange/">AWS Data Exchange</a> service. I was excited to work on a marketplace (<a href="https://artsy.net">Artsy</a> is a marketplace for fine art), and the idea of a service that could connect data providers and data consumers at Amazon scale was big. Plus, I was finally going to learn how Amazon, and especially AWS, was so relentlessly successful, from within.</p> <p>I half-jokingly talked of this as semi-retirement, because being an individual contributor at a F.A.A.N.G. seemed a lot easier than being CTO of a company that had grown from seed stage to Series D, raising $100MM. It was certainly different. Six months into my IC role the <a href="/2019/11/13/aws-data-exchange.html">service had launched</a> and I <a href="/2019/11/17/the-pros-and-cons-of-going-from-management-back-to-ic.html">felt that I had made the right decision</a>. I didn’t write enough code though, and was missing working on open-source (Artsy was <a href="https://artsy.github.io/blog/2019/04/29/how-did-artsy-become-oss-by-default/">open-source by default</a>), so I moved to <a href="https://opensearch.org">OpenSearch</a>, a fork of Elasticsearch sponsored by Amazon, in 2021. That year was <a href="/2022/06/05/a-year-working-on-opensearch-2.0.html">incredibly exciting work</a>, getting the product off the ground, followed by steady growth, culminating with <a href="/2024/09/17/opensearch-joins-the-linux-foundation.html">OpenSearch Project joining the Linux Foundation</a> in 2024. In thinking about what to do next, I was not sure I could beat helping convince a 2.5 trillion dollar company to release control of a key project to a non-profit.</p> <p>What have I learned over the years? If anything, it’s that Amazon is a unique company that is capable of changing its mind, but it’s not for the fainthearted. If you are considering a job there, I recommend it.</p> <p>Allow me to share a few thoughts from my tenure.</p> <ol> <li>Nobody is irreplaceable, but people make all the difference.</li> <li>A successful business is a simple one, and does what customers are asking, and willing to pay for.</li> <li>Writing well is a superpower, and a useful tool for influencing people.</li> <li>If you are passionate about what you’re building, it will not feel like work.</li> <li>Most people will recognize and appreciate your passion, and maybe even give you the benefit of the doubt.</li> <li>There is a time and place where criticism can bring value, be sure you know your intention or value before giving it, or risk losing trust.</li> </ol> <p>Finally, I’m forever grateful for my colleagues, the company leaders, and the Amazon PE community.</p> <p>So, what’s next? We shall see! My criteria for a new role are as follows.</p> <ol> <li>I want to work for a company that has a high NPS score of both employee and customer happiness.</li> <li>I want to continue increasing my time living outside of NYC, especially in Latin or South America, and therefore be able to do much of my focused work remotely.</li> </ol> <p>I recognize that I am privileged and extremely lucky to have opportunities where both of these can be met.</p> <p>Onward!</p> <p><a href="https://code.dblock.org/2025/02/10/leaving-amazon.html">Leaving Amazon</a> was originally published by Daniel Doubrovkine at <a href="https://code.dblock.org">code.dblock.org | tech blog</a> on February 10, 2025.</p> <![CDATA[Should You Work on Weekends?]]> https://code.dblock.org/2025/01/20/should-you-work-on-weekends 2025-01-20T01:00:00+00:00 2025-01-20T01:00:00+00:00 Daniel Doubrovkine https://code.dblock.org dblock@dblock.org <p>Should you work on week-ends?</p> <p><em>tl;dr Only 2% of my paid work was accomplished on weekends, which matches my opinion that you should not.</em></p> <h3 id="collecting-data">Collecting Data</h3> <p>First, let’s try to use data and find out whether I actually work on week-ends. I’ve contributed to the open-source OpenSearch since 2021 as part of my day job, so I tried looking through my GitHub commits, but had hard time excluding forks. I settled on fetching pull requests. The following query collects my PRs for the first two weeks of 2025.</p> <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code> gh search prs <span class="se">\</span> <span class="nt">--author</span> <span class="s2">"@me"</span> <span class="se">\</span> <span class="nt">--created</span> <span class="s2">"2025-01-01..2025-01-20"</span> <span class="se">\</span> <span class="nt">--json</span> repository,number,title,createdAt <span class="se">\</span> <span class="nt">--template</span> <span class="s1">'{{range .}}{{tablerow (timefmt "Monday" .createdAt) .repository.nameWithOwner (printf "#%v" .number) (truncate 20 .title) (timeago .createdAt) }}{{end}}'</span> </code></pre></div></div> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Sunday dblock/discord-strava #32 Lock setup-firefox. 1 day ago Friday dblock/discord-strava #30 Upgrade Firefox i... 3 days ago Tuesday opensearch-project/opensearch-api-specification #793 Warn if file path... 6 days ago Friday opensearch-project/opensearch-php #253 Added optional he... 10 days ago Thursday opensearch-project/opensearch-php #249 Adding @kimpepper... 12 days ago Wednesday dblock/code.dblock.org #139 Meeting Open Sour... 12 days ago Wednesday opensearch-project/opensearch-php #247 Fix up UPGRADING.md. 12 days ago Tuesday opensearch-project/opensearch-api-specification #772 Evaluate payload ... 13 days ago Monday opensearch-project/opensearch-api-specification #768 Fix flaky snapsho... 14 days ago Monday opensearch-project/opensearch-api-specification #767 Upload test spec ... 15 days ago Wednesday dblock/code.dblock.org #137 Only vale modifie... 19 days ago </code></pre></div></div> <p>Repeating this pattern using different date ranges and a larger <code class="language-plaintext highlighter-rouge">--limit</code> I found 1,566 PRs, including 1,264 PRs into the opensearch-project GitHub organization since April 2021, the time of the OpenSearch fork. Excluding PRs made on Saturday and Sunday I got 1,234. It’s a suspicious number (1, 2, 3, 4, really?), but it checks out. The data says that I made 30 PRs on week-ends for work, and 225 PRs not for work. Finally, these exclude the many commits I made into my pet projects directly to <code class="language-plaintext highlighter-rouge">main</code>.</p> <p>We can conclude that only 2% of my paid work was accomplished on weekends.</p> <h3 id="which-day-is-more-productive">Which Day is More Productive?</h3> <p>Here’s the distribution of PRs into opensearch-project (aka work) by day of week.</p> <table> <thead> <tr> <th>Day</th> <th>Count</th> </tr> </thead> <tbody> <tr> <td>Monday</td> <td>221</td> </tr> <tr> <td>Tuesday</td> <td>331</td> </tr> <tr> <td>Wednesday</td> <td>193</td> </tr> <tr> <td>Thursday</td> <td>321</td> </tr> <tr> <td>Friday</td> <td>168</td> </tr> <tr> <td>Saturday</td> <td>17</td> </tr> <tr> <td>Sunday</td> <td>13</td> </tr> </tbody> </table> <p>It looks like Tuesdays and Thursdays are the busiest with fewer results on Friday, which is consistent with my opinion about the benefits of <em>finishing earlier or doing other types of work than code on Fridays</em>. Work is not just pull requests, it’s also attending meetings, and I <a href="/2024/12/20/i-participated-in-2882-meetings-in-5-years.html">participated in 2882 meetings in 5 years</a>.</p> <h3 id="should-you-work-on-weekends">Should You Work on Weekends?</h3> <p>In my opinion, an experienced Engineer or a People Manager working on weekends shows a bad example. It sets an unreasonable expectation that everyone must be available 7 days a week for productive work, reduces the overall velocity of a team by wearing people out, and diminishes the quality of the code we collectively produce. Us, humans, need to be able to step back away from the machine to recharge, and we need time to think, therefore working harder or more hours is usually not correlated with better results.</p> <h3 id="what-about-looming-deadlines">What About Looming Deadlines?</h3> <p>Over the years I observed that developers that bang out code on a Sunday tend to bang out a lot less code on Monday. Those on-call on weekends are exhausted the entire week. Individuals that can’t find time to focus on code during the work week tend to compensate by coding on weekends, which demonstrates that they are unproductive during the week - a net negative. Having to work on a weekend to meet a deadline is usually a failure of planning. There are certainly exceptions, but they should be exceedingly rare.</p> <h3 id="what-about-pet-projects-and-open-source">What About Pet Projects and Open-Source?</h3> <p>Pet projects and open-source contributions outside of paid work are a wonderful source of independence and dopamine, I have <a href="https://www.vestris.com/">several</a>. I like doing that type of work on weekends, but only for a small amount of hours, and never feeling that it takes away from my main week-end activities that include spending time with family or friends. I think this attitude evolved over the years as I used to spend a lot of time coding on week-ends, but I believe I have consistently tried to make sure that was not paid work that I <em>had to do</em>. Finally, I am lucky to be paid to do open-source work, but I absolutely consider that <em>work</em> and avoid doing it outside of work hours.</p> <h3 id="creating-discipline">Creating Discipline</h3> <p>Not working on weekends and not burning out requires discipline. If you find it difficult to detach yourself from the computer on weekends, begin your Saturday and Sunday with the hardest thing to do. For me, this meant forcing myself to open a book before my laptop, or putting my shoes on and going for a long run, first thing in the morning.</p> <p>You can do it!</p> <p><a href="https://code.dblock.org/2025/01/20/should-you-work-on-weekends.html">Should You Work on Weekends?</a> was originally published by Daniel Doubrovkine at <a href="https://code.dblock.org">code.dblock.org | tech blog</a> on January 20, 2025.</p> <![CDATA[Meeting Open Source Users Where They Are]]> https://code.dblock.org/2025/01/08/meeting-open-source-users-where-they-are 2025-01-08T01:00:00+00:00 2025-01-08T01:00:00+00:00 Daniel Doubrovkine https://code.dblock.org dblock@dblock.org <p>Most paying customers expect a certain level of (paid) support. One opens a ticket, gets a response, and any issue can be resolved.</p> <p>How does “support” work in open-source? My rule of thumb is to meet users <em>where they are</em>.</p> <p>GitHub projects have issues, but opening an issue to ask a question is generally frowned upon. Therefore, in JNA, we have a <a href="https://groups.google.com/g/jna-users">mailing list</a>, and in OpenSearch, we have <a href="https://forum.opensearch.org">a forum</a> and <a href="https://opensearch.org/slack.html">Slack</a>. StackOverflow is quite popular, too.</p> <p>For a small project I choose <strong>GitHub issues</strong> as the preferred and only channel. It keeps things well organized and is the least amount of overhead for both users and maintainers. Most questions can be turned into either an issue (e.g. <em>“I have tried this, and did not get the expected results.”</em>), or a feature request (e.g. <em>“Would it be possible to add a feature that works as follows?”</em>). I like to use templates, such as <a href="https://github.com/opensearch-project/.github/tree/main/.github/ISSUE_TEMPLATE">these</a> to help guide users in opening issues with enough information to get meaningful help.</p> <p>For a larger project that has multiple areas of interest I like <strong>Slack</strong> with dedicated channels for various areas, such as the <a href="https://opensearch.slack.com/archives/C051XNH62KB">#clients</a> channel on the OpenSearch public Slack. As a (co)maintainer of OpenSearch clients my responsibilities are as follows.</p> <ol> <li><strong>Create the channel</strong>. This is a public channel so that anyone in the workspace can join and participate, someone has to make one.</li> <li><strong>Set channel purpose and topic</strong>. Clearly define the purpose and topic of the channel. For example, the purpose could be <em>“A space for discussing contributions to our project, including bug reports, feature requests, and code contributions.”</em> The topic can provide a brief overview of the contribution guidelines and processes.</li> <li><strong>Pin important messages</strong>. Pin any important messages or documents related to contributing to the project, such as the contribution guidelines, code of conduct, or relevant links to your project’s repository or issue tracker.</li> <li><strong>Encourage participation</strong>. Announce the new channel to community members in #general and encourage everyone to join and actively participate in the conversation. You can share the channel link or invite them directly.</li> <li><strong>Moderate and respond</strong>. Monitor the channel regularly. Respond to questions, provide guidance, and ensure that discussions remain constructive and aligned with project’s contribution guidelines. I usually sweep my channels bi-weekly and respond to any message that has not been answered by someone else.</li> <li><strong>Triage and assign issues</strong>. When contributors report bugs or suggest new features, ask them to create issues in project’s issue tracker, and share the links in the channel to close the loop. Work with the members of the channel to try and assign someone to investigate and address these issues when they are willing to help.</li> <li><strong>Celebrate contributions</strong>. Recognize and celebrate contributions from community members. Share updates on merged pull requests, thank contributors, or highlight notable contributions in the channel.</li> <li><strong>Provide feedback</strong>. Encourage contributors to share their experiences and provide feedback on the contribution process. Use this feedback to improve the guidelines, documentation, or processes as needed.</li> <li><strong>Share updates and announcements</strong>. Use the channel to share releases, updates, announcements, or changes related to your project’s contribution processes or guidelines.</li> </ol> <p>An interesting aspect of the open-source OpenSearch is that it’s also offered as a paid managed service on AWS. Therefore, it’s common for an AWS customer to ask a question about using OpenSearch in the managed environment, which is mostly identical to using the open-source product. Many such customers are willing to do some work and will often help narrow the problem down by trying the open-source version. Such customers always appreciate the closer feedback loop of the developer community on Slack, and the ability to resolve their issues in code by themselves, which is another example of meeting your <del>users</del> customers where they are.</p> <p><a href="https://code.dblock.org/2025/01/08/meeting-open-source-users-where-they-are.html">Meeting Open Source Users Where They Are</a> was originally published by Daniel Doubrovkine at <a href="https://code.dblock.org">code.dblock.org | tech blog</a> on January 08, 2025.</p> <![CDATA[Adding a Spell Checker to a Jekyll Blog]]> https://code.dblock.org/2024/12/30/adding-a-spell-checker-to-a-jekyll-blog 2024-12-30T01:00:00+00:00 2024-12-30T01:00:00+00:00 Daniel Doubrovkine https://code.dblock.org dblock@dblock.org <p>I found it annoyingly non-trivial to add a spell checker to this blog.</p> <p>For now, I settled on <a href="https://github.com/marketplace/actions/github-spellcheck-action#extra-configuration-for-markdown">GitHub Spellcheck Action</a> that uses <a href="https://facelessuser.github.io/pyspelling">PySpelling</a> on files changed in the commit or pull request as described in <a href="https://swild.dev/self-hosting/github-spellcheck-lint-action/">this blog post</a>.</p> <div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">name</span><span class="pi">:</span> <span class="s">Check Spelling</span> <span class="na">on</span><span class="pi">:</span> <span class="pi">[</span><span class="nv">push</span><span class="pi">,</span> <span class="nv">pull_request</span><span class="pi">]</span> <span class="na">jobs</span><span class="pi">:</span> <span class="na">check</span><span class="pi">:</span> <span class="na">runs-on</span><span class="pi">:</span> <span class="s">ubuntu-latest</span> <span class="na">steps</span><span class="pi">:</span> <span class="pi">-</span> <span class="na">uses</span><span class="pi">:</span> <span class="s">actions/checkout@v3</span> <span class="pi">-</span> <span class="na">uses</span><span class="pi">:</span> <span class="s">tj-actions/changed-files@v46.0.4</span> <span class="na">id</span><span class="pi">:</span> <span class="s">changed_files</span> <span class="na">with</span><span class="pi">:</span> <span class="na">files</span><span class="pi">:</span> <span class="pi">|</span> <span class="s">**/**.md</span> <span class="s">**/**.markdown</span> <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Check Spelling</span> <span class="na">uses</span><span class="pi">:</span> <span class="s">rojopolis/spellcheck-github-actions@0.45.0</span> <span class="na">with</span><span class="pi">:</span> <span class="na">task_name</span><span class="pi">:</span> <span class="s">Markdown</span> <span class="na">config_path</span><span class="pi">:</span> <span class="s">.pyspelling.yml</span> <span class="na">source_files</span><span class="pi">:</span> <span class="s">${{ steps.changed_files.outputs.all_changed_files }}</span> </code></pre></div></div> <p>To run PySpelling locally ensure you have a working version of Python, install PySpelling with <code class="language-plaintext highlighter-rouge">pip install pyspelling</code>, and <a href="http://aspell.net/">aspell</a> with <code class="language-plaintext highlighter-rouge">brew install aspell</code> on a Mac. In <a href="https://github.com/dblock/code.dblock.org/blob/gh-pages/.pyspelling.yml">my configuration</a> I also use <code class="language-plaintext highlighter-rouge">pymdownx</code> from <a href="https://facelessuser.github.io/pymdown-extensions">pymdown-extensions</a> which is installed with <code class="language-plaintext highlighter-rouge">pip install pymdown-extensions</code>.</p> <p>You need a <a href="https://github.com/dblock/code.dblock.org/blob/gh-pages/.pyspelling.yml">.pyspelling.yml</a> and you can run it as follows.</p> <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>pyspelling <span class="nt">--config</span> .pyspelling.yml </code></pre></div></div> <p>This is a Jekyll blog in which we want to ignore code, wrapped between Jekyll magic commands for syntax highlighting. This can be accomplished with a PySpelling pipeline in the above-mentioned configuration file.</p> <div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">pipeline</span><span class="pi">:</span> <span class="pi">-</span> <span class="na">pyspelling.filters.context</span><span class="pi">:</span> <span class="na">context_visible_first</span><span class="pi">:</span> <span class="no">true</span> <span class="na">delimiters</span><span class="pi">:</span> <span class="c1"># ignore jekyll multiline magic highlights {% ... %}</span> <span class="pi">-</span> <span class="na">open</span><span class="pi">:</span> <span class="s1">'</span><span class="s">(?s)^\{\%</span><span class="nv"> </span><span class="s">highlight</span><span class="nv"> </span><span class="s">.*</span><span class="nv"> </span><span class="s">\%\}$'</span> <span class="na">close</span><span class="pi">:</span> <span class="s1">'</span><span class="s">^\{\%</span><span class="nv"> </span><span class="s">endhighlight</span><span class="nv"> </span><span class="s">\%\}$'</span> <span class="c1"># ignore the rest of jekyll magic commands</span> <span class="pi">-</span> <span class="na">open</span><span class="pi">:</span> <span class="s1">'</span><span class="s">{%'</span> <span class="na">close</span><span class="pi">:</span> <span class="s1">'</span><span class="s">%}'</span> </code></pre></div></div> <p>Finally, we can collect the initial set of words to potentially exclude from existing posts into <a href="https://github.com/dblock/code.dblock.org/blob/gh-pages/.pyspelling.words">.pyspelling.words</a>.</p> <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>pyspelling <span class="nt">--config</span> .pyspelling.yml | <span class="se">\</span> <span class="nb">sed</span> <span class="nt">-n</span> <span class="s1">'/^Misspelled words:$/,/^-*$/!p'</span> | <span class="se">\</span> <span class="nb">grep</span> <span class="nt">-v</span> <span class="s2">"^---</span><span class="se">\+</span><span class="s2">$"</span> | <span class="se">\</span> <span class="nb">grep</span> <span class="nt">-v</span> <span class="s2">"^$"</span> | <span class="se">\</span> <span class="nb">sort</span> | <span class="se">\</span> <span class="nb">uniq</span> <span class="o">&gt;</span> .pyspelling.words </code></pre></div></div> <p>The file helped me spot a few spelling mistakes, now fixed. See <a href="https://github.com/dblock/code.dblock.org/pull/134">code.dblock.org#134</a> for the full change.</p> <p>Update: I eventually switched to using regular backticks for code when adding a style checker in <a href="https://github.com/dblock/code.dblock.org/pull/136">#136</a>, so the above <code class="language-plaintext highlighter-rouge">delimiters</code> rule changed as follows.</p> <div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="pi">-</span> <span class="na">pyspelling.filters.context</span><span class="pi">:</span> <span class="na">context_visible_first</span><span class="pi">:</span> <span class="no">true</span> <span class="na">escapes</span><span class="pi">:</span> <span class="s">\\[\\`~]</span> <span class="na">delimiters</span><span class="pi">:</span> <span class="pi">-</span> <span class="na">open</span><span class="pi">:</span> <span class="s2">"</span><span class="s">(?s)^(?P&lt;open&gt;</span><span class="nv"> </span><span class="s">*`{3,})([a-z+]*)$"</span> <span class="na">close</span><span class="pi">:</span> <span class="s2">"</span><span class="s">^(?P=open)$"</span> <span class="pi">-</span> <span class="na">open</span><span class="pi">:</span> <span class="s2">"</span><span class="s">(?P&lt;open&gt;`+)"</span> <span class="na">close</span><span class="pi">:</span> <span class="s2">"</span><span class="s">(?P=open)"</span> <span class="pi">-</span> <span class="na">open</span><span class="pi">:</span> <span class="s1">'</span><span class="s">{%'</span> <span class="na">close</span><span class="pi">:</span> <span class="s1">'</span><span class="s">%}'</span> </code></pre></div></div> <p><a href="https://code.dblock.org/2024/12/30/adding-a-spell-checker-to-a-jekyll-blog.html">Adding a Spell Checker to a Jekyll Blog</a> was originally published by Daniel Doubrovkine at <a href="https://code.dblock.org">code.dblock.org | tech blog</a> on December 30, 2024.</p> <![CDATA[I Participated in 2882 Meetings in 5 Years]]> https://code.dblock.org/2024/12/20/i-participated-in-2882-meetings-in-5-years 2024-12-20T01:00:00+00:00 2024-12-20T01:00:00+00:00 Daniel Doubrovkine https://code.dblock.org dblock@dblock.org <p>Since my first day in AWS 5 1/2 years ago, I’ve been experimenting with keeping a CHANGELOG of everything I do, available for everyone at the company to see. I wrote about it <a href="/2020/09/01/keep-a-changelog-at-work.html">here</a>. If you work at AWS, use the URL in the screenshot below to find it.</p> <p><img src="/images/posts/2020/2020-09-01-keep-a-changelog-at-work/onboarding.png" alt="" /></p> <p>I wrote a little script to try and classify where I spent my time. Here are some quick, very unscientific results.</p> <p>I participated in <strong>2882 meetings</strong>, 75% were probably technical discussions and 25% were not, including business reviews. I made <strong>1013</strong> code contributions, reviewed or read <strong>364</strong> documents, mostly technical designs. I did <strong>192 1:1s</strong> with my colleagues, <strong>130</strong> interviews, and helped with <strong>80 promos</strong>. I debugged or otherwise dealt with <strong>175 customer-facing issues</strong>, authored <strong>78 documents</strong>, took <strong>64 trainings</strong>, and engaged in <strong>48</strong> planning exercises.</p> <p>This, of course, doesn’t tell you anything useful. If you actually want to learn more about what PEs do at Amazon, check out the excellent <a href="https://www.linkedin.com/pulse/principal-engineer-roles-framework-mai-lan-tomsen-bukovec-142df/">Principal Engineer Roles Framework</a> that I have been using since 2019 and that was made public this week by Mai-Lan Tomsen Bukovec.</p> <p><a href="https://code.dblock.org/2024/12/20/i-participated-in-2882-meetings-in-5-years.html">I Participated in 2882 Meetings in 5 Years</a> was originally published by Daniel Doubrovkine at <a href="https://code.dblock.org">code.dblock.org | tech blog</a> on December 20, 2024.</p> <![CDATA[Do Not Fix Bugs Reported in Your Open Source Projects]]> https://code.dblock.org/2024/12/19/do-not-fix-bugs-reported-in-your-open-source-projects 2024-12-19T01:00:00+00:00 2024-12-19T01:00:00+00:00 Daniel Doubrovkine https://code.dblock.org dblock@dblock.org <p>Have you watched <a href="https://en.wikipedia.org/wiki/The_IT_Crowd">The IT Crowd</a>? It’s a hilarious British television sitcom from around 2006 that cast a bunch of IT geniuses at the Reynholm Industries tech support department in London. One of the signature laughs is that every time the phone rang, Roy would pick it up and without waiting say “Have you turned it off and on again?”, then hang up. I often feel like Roy when engaging with users reporting bugs in open-source projects I maintain.</p> <p>Here’s my structured approach for any bug being reported in my open-source projects.</p> <ol> <li>Do not fix the bug. Because this is open source, and we are not selling software, maintainers may have some social responsibility, but are under no obligation to do anything.</li> <li>Do not try to reproduce the bug. Not convinced it’s a bug? Cannot reproduce the issue? Politely request additional information or clarification.</li> <li>Do not try to write a test that proves this is a bug. Ask the person reporting the issue to try and write the automated and failing test. This helps narrow down the problem and ensures that the bug stays fixed in the future.</li> <li>Do not fix the bug. Now that the person reporting the bug has written an automated test for it, they are <em>this</em> close from fixing it. Ask them to do it. This gives the individual reporting the bug a sense of ownership and contribution to the project.</li> <li>Do not do anything else. Can’t get any engagement on the bug from the person reporting it? Leave the bug open. Someone else will pick it up.</li> </ol> <p>tl;dr Do not fix the bug! A healthy open-source project will have lots of engaged contributors, especially when it comes to bugs. This is one of the <a href="/2024/12/18/harvesting-the-low-hanging-fruit-in-open-source-projects.html">lowest hanging fruit</a> that you can harvest as a maintainer. That said, sometimes I just want to fix the bug myself because it’s so interesting. So YMMV.</p> <p><a href="https://code.dblock.org/2024/12/19/do-not-fix-bugs-reported-in-your-open-source-projects.html">Do Not Fix Bugs Reported in Your Open Source Projects</a> was originally published by Daniel Doubrovkine at <a href="https://code.dblock.org">code.dblock.org | tech blog</a> on December 19, 2024.</p> <![CDATA[Harvesting the Low Hanging Fruit in Open Source Projects]]> https://code.dblock.org/2024/12/18/harvesting-the-low-hanging-fruit-in-open-source-projects 2024-12-18T01:00:00+00:00 2024-12-18T01:00:00+00:00 Daniel Doubrovkine https://code.dblock.org dblock@dblock.org <p>Every active open-source project grows a lot of low hanging fruit. Encouraging project users to harvest some of the yield can be a great way to engage and retain new contributors. Here are some ideas, mostly borrowed from <a href="https://github.com/bbarani">Barani</a>, for low-hanging work items that you can propose to anyone engaging in your project.</p> <hr /> <h4 id="help-wanted-good-first-issue-and-summer-of-code">Help Wanted, Good First Issue and Summer of Code</h4> <p>Identify and tag issues that have an easy solution to attract first time contributors.</p> <p>Good first issues, or issues that are part of programs such as <a href="https://summerofcode.withgoogle.com/">Summer of Code</a>, provide an entry point for new contributors who want to get involved with the project, but are intimidated or unsure where to start. These issues are typically well-defined, self-contained, and relatively straightforward, making them accessible to newcomers.</p> <p>I could use help with <a href="https://github.com/issues?q=is%3Aopen+is%3Aissue+author%3Adblock+archived%3Afalse+label%3A%22good+first+issue%22%2C%22help+wanted%22">58 issues</a> right now and there are <a href="https://github.com/issues?q=is%3Aopen+is%3Aissue+archived%3Afalse+org%3Aopensearch-project+label%3A%22good+first+issue%22">683</a> issues labelled “good first issue” in the opensearch-project GitHub organization.</p> <h4 id="readmes-and-documentation">READMEs and Documentation</h4> <p>README.md, documentation and “Getting Started” for developers are often outdated or overlooked in open-source projects. Contributors can begin by fixing typos, improving clarity, or adding examples to existing documentation. This is a great way for new contributors to get familiar with the project’s codebase and contribute without making significant code changes.</p> <p>The recent <a href="/2024/09/17/opensearch-joins-the-linux-foundation.html">transfer of the OpenSearch Project to the Linux Foundation</a> required a change the code-of-conduct violations e-mail address in <code class="language-plaintext highlighter-rouge">CODE_OF_CONDUCT.md</code> in <a href="https://github.com/opensearch-project/.github/pull/245">opensearch-project/.github#245</a>. The same, trivial change needs to happen in 116 repositories. We can get some help with <a href="https://github.com/opensearch-project/project-meta?tab=readme-ov-file#make-a-change-in-many-repos">automation</a>, but someone still needs to do it.</p> <h4 id="updating-dependencies">Updating Dependencies</h4> <p>Most code projects rely on external dependencies, and keeping them up-to-date is crucial for security and compatibility reasons. Contributors can help by identifying outdated dependencies and submitting pull requests to update them to the latest versions.</p> <p>Almost all projects in the <a href="https://github.com/mongoid">mongoid GitHub organization</a> would appreciate PRs that add support for newer versions of MongoDB, which is most likely no work other than adding new versions to the CI matrix similar to <a href="https://github.com/mongoid/mongoid-scroll/pull/45">mongoid-scroll#45</a>. Another example is upgrading Gradle in OpenSearch, such as <a href="https://github.com/opensearch-project/OpenSearch/pull/16694">OpenSearch#16694</a>, something that had to be done over 100 times over the last 3 years and could not be automated because it required minor code changes along the way.</p> <h4 id="improving-test-coverage">Improving Test Coverage</h4> <p>Writing tests is an essential part of software development, but it’s not uncommon for open-source projects to have gaps in their test coverage or not tests at all. Contributors can help by adding new test cases or improving existing ones, ensuring that the project’s codebase is thoroughly tested. Adding code coverage is another easy contribution.</p> <p>I have recently opened <a href="https://github.com/bluesky-social/statusphere-example-app/pull/26">statusphere-example-app#26</a> that added tests to the atproto boilerplate example and <a href="https://github.com/mongoid/mongoid-scroll/pull/47">mongoid-scroll#47</a> that added test coverage.</p> <h4 id="refactoring-code">Refactoring Code</h4> <p>As projects evolve, their codebase can become messy and difficult to maintain. Contributors can help by identifying areas that need refactoring and submitting pull requests to improve code readability, maintainability, and performance.</p> <p><a href="https://github.com/ericproulx">Eric</a>, one of the maintainers of ruby-grape has been steadily improving the library performance such as in <a href="https://github.com/ruby-grape/grape/pull/2513">grape#2513</a>, or refactoring various parts such as <a href="https://github.com/ruby-grape/grape/pull/2475">grape#2475</a>.</p> <h4 id="fixing-linting-errors">Fixing Linting Errors</h4> <p>Most projects have automated linting rules in place to enforce coding standards and best practices. Contributors can help by identifying and fixing linting errors, ensuring that the codebase adheres to the project’s coding standards.</p> <p>You can begin by adding a linter if there isn’t one such as <a href="https://github.com/opensearch-project/opensearch-api-specification/pull/312">opensearch-api-specification#312</a>, upgrade RuboCop in Ruby projects such as <a href="https://github.com/ruby-grape/grape/pull/2497">grape#2497</a>, or fix actual violations being ignored in <code class="language-plaintext highlighter-rouge">.rubocop_todo.yml</code> such as in <a href="https://github.com/ruby-grape/grape/pull/2459">grape#2459</a>.</p> <h4 id="reporting-and-engaging-on-issues">Reporting and Engaging on Issues</h4> <p>Contributors can help by reporting new issues, reproducing existing ones, moving issues forward, or helping categorize issues to help maintainers prioritize their work.</p> <p>For example, I figured out how to reproduce a rare bug in logstash-output-opensearch in <a href="https://github.com/opensearch-project/logstash-output-opensearch/issues/207#issuecomment-1688616636">logstash-output-opensearch#207</a>, and helped a user enable verbose logging to debug another problem in <a href="https://github.com/opensearch-project/logstash-output-opensearch/issues/187#issuecomment-1765055939">logstash-output-opensearch#187</a>.</p> <h4 id="creating-examples-or-tutorials">Creating Examples or Tutorials</h4> <p>Providing examples and tutorials can make it easier for new users to understand and adopt a project. Contributors can create new examples, tutorials, or sample projects to help others get started with the project.</p> <p>For example, as I was researching various vector database APIs, I <a href="https://code.dblock.org/2023/06/16/getting-started-with-vector-dbs-in-python.html">wrote a set of working samples</a> for every database. I also implemented an example of using the OpenSearch async Python client with AWS in <a href="https://github.com/opensearch-project/opensearch-py/pull/857">opensearch-py#857</a> when someone asked me for help.</p> <h4 id="improving-project-infrastructure">Improving Project Infrastructure</h4> <p>Open-source projects often rely on various tools and infrastructure, such as continuous integration, deployment pipelines, or issue tracking systems. Contributors can help by identifying areas for improvement and submitting pull requests to enhance the project’s infrastructure.</p> <p>For example, I added a feature that makes CI fail when someone accidentally forgets to reference a test suite in the test matrix in <a href="https://github.com/opensearch-project/opensearch-api-specification/pull/705">opensearch-api-specification#705</a> after someone else came up with this idea.</p> <h4 id="reviewing-code">Reviewing Code</h4> <p>Reviewing other people’s code could be easier to start with than actual coding. Even they may not have all the knowledge and review all the change, they may still be able to point out some common issues or raise good questions. If their review proves to be valuable to others and with more and more knowledge, they may think of starting contribution as well.</p> <p>For example, an experienced PHP developer that has never contributed to the project found work in progress by another developer for a problem they had, and jumped in help review it <a href="https://github.com/opensearch-project/opensearch-php/pull/233">opensearch-php#233</a>. I gently nudge both the contributor and the reviewer to get that PR finally merged.</p> <hr /> <p>In conclusion, by proposing these low-hanging work items, we can encourage new contributors to get involved and make meaningful contributions to open-source projects. It’s important to provide clear guidelines, support, and mentorship to help contributors navigate the project’s codebase and contribution process. So go ahead, engage with a new project participant, ask them whether they would be interested in, and gently guide them to make their first contribution.</p> <p><a href="https://code.dblock.org/2024/12/18/harvesting-the-low-hanging-fruit-in-open-source-projects.html">Harvesting the Low Hanging Fruit in Open Source Projects</a> was originally published by Daniel Doubrovkine at <a href="https://code.dblock.org">code.dblock.org | tech blog</a> on December 18, 2024.</p> <![CDATA[How to Grow Open-Source Contributors and Maintainers]]> https://code.dblock.org/2024/12/14/how-to-grow-open-source-contributors-and-maintainers 2024-12-14T01:00:00+00:00 2024-12-14T01:00:00+00:00 Daniel Doubrovkine https://code.dblock.org dblock@dblock.org <p>I’ve been very active in open source since around 2008 and have become very passionate about growing contributors and maintainers in the projects I am involved in. The health of an open-source project can easily be measured by the number of actively engaged participants in its code, which cannot happen without maintainers. Maintainers are leaders driving open-source projects. The best maintainers contribute significant code, have the responsibility to review other people’s contributions, and perform the release process. In my experience, the best ones are experts in the system they are developing. They are prolific coders, and are able to collaborate with contributors that don’t work for the same business, maintain a high quality bar, sweat the small stuff, and recognize behaviors that earn trust in others. The goal of a project maintainer is to always improve the project, ensure it stays the course of its core mission, and to work themselves out of the job by promoting contributors to maintainer positions. This is often referred to as <em>meritocracy</em>.</p> <p>I hold two possibly controversial opinions. First, to attract long lasting contributors that can become maintainers a project needs <strong>an owner</strong>, sometimes known as a benevolent dictator. Second, contributors need to have <strong>a business need</strong>, or an unusually strong personal interest to stick around.</p> <p>I have yet to see it work differently, and I will illustrate this with some examples from my own experience.</p> <p>In 2010 I was working a database security analytics product. We were paying an exorbitant amount of money for a software license to get basic hardware information. To save costs I created <a href="/2010/06/23/introducing-oshi-operating-system-and-hardware-information-java.html">OSHI</a>, an operating systems hardware information library written in Java. The first contributor to the project came in 2013, a handful in 2015, 8 became project co-maintainers, and one emerged as the defacto benevolent dictator by 2018, with 201 people contributing to 158 releases as of today. It is the defacto cross-platform standard for enumerating hardware components in Java.</p> <p>The same year our Enterprise product needed a single-sign-on solution for our Java web application, so I created and open-sourced <a href="/2010/03/01/waffle-windows-authentication-functional-framework-le.html">Waffle</a>, which is based on <a href="https://github.com/java-native-access/jna">Java Native Access</a> that I began contributing to, quickly becoming JNA’s co-maintainer. Today, waffle has 4 maintainers, a new benevolent dictator since 2014, and 52 people have contributed to 22 releases. The JNA project is hugely popular, used in anything from OpenSearch to IntelliJ, but only has 9 maintainers, is on its 4th benevolent dictator, and 166 people have contributed to 65 releases.</p> <p>In 2011, while building Artsy, we standardized on <a href="https://github.com/ruby-grape/grape">Grape</a>, a RESTful API framework alternative to Ruby-on-Rails. I began contributing to the project and earned maintainer rights to it around 2013, eventually becoming the benevolent dictator. I created <a href="/2015/08/04/ruby-grape.html">the ruby-grape organization</a>, and brought in a number of related projects together in 2015. The core Grape project has 7 maintainers today, and 426 people have contributed to 73 releases.</p> <p>Finally, in 2019 I joined the <a href="https://opensearch.org/">OpenSearch Project</a> team at Amazon to work on the fork that powers the Amazon Managed and Serverless OpenSearch service. The project is now part of the <a href="/2024/09/17/opensearch-joins-the-linux-foundation.html">Linux Foundation</a>. I am an active co-maintainer of 24 out of 116 repositories in the opensearch-project org on GitHub and benevolent dictator in a few. It has 257 maintainers today, and 1,402 people have contributed to hundreds of releases across the distribution of OpenSearch and OpenSearch Dashboards, ingestion tools, and clients.</p> <p>To understand the patterns behind these stories, let’s examine how projects attract contributors and grow them into maintainers.</p> <p>Early this year I reached out to the existing maintainers across opensearch-project that did not work for Amazon, and asked them questions about how they became maintainers. 20 have replied. I learned that most came to the project as their businesses were migrating from Elasticsearch to OpenSearch, often because their employer had business needs in OpenSearch, and remained active because they were pleased with how well-maintained, healthy, and high quality these projects were.</p> <p>This is the recipe that we have followed for 3 years in the repos that I have been involved in.</p> <ol> <li>At least one highly engaged maintainer that acts as the project owner or benevolent dictator, and routinely commits code to the repo.</li> <li>Open-source basics including a license, code-of-conduct, security and copyright files and statements.</li> <li>A well-written README.md, project documentation, and a developer guide that one can easily follow to begin contributing without help.</li> <li>High test coverage, code linters, and a reliably passing CI/CD.</li> <li>Engagement from at least one maintainer on every single issue, pull request or comment within 24 hours.</li> <li>Visible recent commit activity, even if just from a dependency manager such as dependabot.</li> <li>Regular releases, a CHANGELOG or release notes.</li> </ol> <p>I often step up to do this in repos I am engaged in and, naturally, in all repos I create. My job as a maintainer is to make all the above points happen as a prerequisite to anyone engaging in a project.</p> <p>A user with a business need will typically open an issue before contributing code. As a maintainer, I will respond to their inquiry in a way that encourages contribution, even a minimal one. For example, if a user opens a bug, I do not jump in to fix it, especially if it’s an easy fix. My typical response is: <em>Thank you for reporting this issue! Looks like a bug. Would you like to help? Maybe write a (failing) test that reproduces this issue?</em> Contributors that write a test often also fix the bug. If a user asks how to do something, I help them and ask them to contribute a sample or additional documentation.</p> <p>When reviewing pull requests (always within 24 hours) my goal is to get the contribution merged. How can I say <em>yes</em> more often? I try to make sure I am clear about what change is a <em>must have</em>, <em>should have</em>, or <em>nice to have</em>. I reserve must have’s for critical misses (e.g. poor APIs that will be hard to change later), and always sweat the small stuff and insist on highest standards (e.g. tests and documentation are always required). This is because one’s contribution is never allowed to create more work or technical debt for me or other participants in the project. Finally, I often thank contributors profusely for hanging in there with me through multiple iterations, and apologize for my excessive OCD over periods at the end of sentences. In my experience people take this attention to detail really well and are motivated to contribute to a project that keeps the quality high, often higher than their projects at work that make catastrophic technical compromises to reduce time to market.</p> <p>At last, if you made any significant, high quality, contribution to one of my open-source projects, and have a decent contribution history on Github, I will ask you to join as a co-maintainer quickly. The bar is quite low, I just need to see that you can be trusted to exhibit the behavior described above as a maintainer. In some ways, this ask is a non-automated, higher touch, softer implementation of the <a href="https://github.com/Moya/contributors">Moya Contributing Guidelines</a>, which I wrote about <a href="/2020/06/08/i-ve-asked-you-to-become-an-open-source-project-co-maintainer.html">here</a>. When people step up I usually step out and let them take over, working myself out of the job. Thus, a strong community of contributors grows around the project, which is the real measure of success.</p> <p>Do you follow a similar recipe? Have you see open-source projects emerge and succeed in different ways? Let me know!</p> <p><a href="https://code.dblock.org/2024/12/14/how-to-grow-open-source-contributors-and-maintainers.html">How to Grow Open-Source Contributors and Maintainers</a> was originally published by Daniel Doubrovkine at <a href="https://code.dblock.org">code.dblock.org | tech blog</a> on December 14, 2024.</p> <![CDATA[Creating an OpenSearch Cluster with Dangling Indexes]]> https://code.dblock.org/2024/11/21/creating-an-opensearch-cluster-with-dangling-indexes 2024-11-21T01:00:00+00:00 2024-11-21T01:00:00+00:00 Daniel Doubrovkine https://code.dblock.org dblock@dblock.org <p>I’ve previously <a href="/2024/07/02/using-opensearch-cat-api-response-formats.html">written</a> about the <a href="https://github.com/opensearch-project/opensearch-api-specification">OpenSearch OpenAPI Specification</a> and how <a href="/2024/07/12/fixing-bugs-in-opensearch-api-documentation-using-api-spec-tests.html">it can be used to fix bugs in the OpenSearch documentation</a>.</p> <p>Most of the tests in the API spec work with a <a href="https://github.com/opensearch-project/opensearch-api-specification/blob/main/tests/default/docker-compose.yml">generic Docker container</a> and some basic network setup to expose a local 9200 port.</p> <div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">version</span><span class="pi">:</span> <span class="s1">'</span><span class="s">3'</span> <span class="na">services</span><span class="pi">:</span> <span class="na">opensearch-cluster</span><span class="pi">:</span> <span class="na">image</span><span class="pi">:</span> <span class="s">opensearchproject/opensearch:latest</span> <span class="na">ports</span><span class="pi">:</span> <span class="pi">-</span> <span class="s">9200:9200</span> <span class="pi">-</span> <span class="s">9600:9600</span> <span class="na">environment</span><span class="pi">:</span> <span class="pi">-</span> <span class="s">OPENSEARCH_JAVA_OPTS=-Xms512m -Xmx512m</span> <span class="pi">-</span> <span class="s">discovery.type=single-node</span> </code></pre></div></div> <p>When testing an optional plugin we install it in Dockerfile, such as <a href="https://github.com/opensearch-project/opensearch-api-specification/tree/main/tests/plugins/analysis">in the following analysis test suite</a>.</p> <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ARG OPENSEARCH_DOCKER_HUB_PROJECT ARG OPENSEARCH_VERSION ARG OPENSEARCH_DOCKER_REF FROM opensearchproject/opensearch:latest RUN ./bin/opensearch-plugin <span class="nb">install</span> <span class="nt">-b</span> analysis-icu RUN ./bin/opensearch-plugin <span class="nb">install</span> <span class="nt">-b</span> analysis-phonenumber </code></pre></div></div> <p>The <a href="https://github.com/opensearch-project/opensearch-api-specification/blob/main/tests/plugins/analysis/docker-compose.yml">docker-compose.yml</a> references this Dockerfile in <code class="language-plaintext highlighter-rouge">build</code>.</p> <div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code> <span class="na">opensearch-cluster</span><span class="pi">:</span> <span class="na">build</span><span class="pi">:</span> <span class="na">context</span><span class="pi">:</span> <span class="s">.</span> </code></pre></div></div> <p>Testing more advanced APIs in OpenSearch requires a setup that also imports data. For example, OpenSearch has a concept of so-called <a href="https://opensearch.org/docs/latest/api-reference/index-apis/dangling-index/">dangling indexes</a>. After a node joins a cluster, dangling indexes occur if any shards exist in the node’s local directory that do not already exist in the cluster. To fabricate a dangling index one must create a cluster that loses quorum. Fortunately, we have available to us <a href="https://www.youtube.com/watch?v=SWjCq0xTN7w">an excellent talk by Amitai Stern</a> that goes into great depth into data migrations between clusters and talks about a method called “node toss” at around 17:50 that puts node indexes into a dangling state. Amitai has also authored <a href="https://github.com/opensearch-project/opensearch-api-specification/blob/20b3b04dd6a998929e01e6494d4ac897ac36eaff/tests/dangling/.data/README.md">a cookbook</a> for re-creating a dangling index in a Docker container.</p> <p>Here’s how it works.</p> <p>We begin by creating a single-node cluster without security that uses a local directory (<code class="language-plaintext highlighter-rouge">opensearch-data1</code>) to store data. Note the <code class="language-plaintext highlighter-rouge">--entrypoint /bin/bash</code> which opens a shell instead of starting the cluster.</p> <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>docker run <span class="nt">--name</span> opensearch-single-node-cluster<span class="se">\</span> <span class="nt">-it</span> <span class="nt">--entrypoint</span> /bin/bash <span class="se">\</span> <span class="nt">-p</span> 9200:9200 <span class="nt">-p</span> 9600:9600 <span class="se">\</span> <span class="nt">-e</span> <span class="s2">"discovery.type=single-node"</span> <span class="se">\</span> <span class="nt">--ulimit</span> <span class="nv">memlock</span><span class="o">=</span><span class="nt">-1</span>:-1 <span class="se">\</span> <span class="nt">--ulimit</span> <span class="nv">nofile</span><span class="o">=</span>65536:65536 <span class="se">\</span> <span class="nt">-e</span> <span class="nv">DISABLE_INSTALL_DEMO_CONFIG</span><span class="o">=</span><span class="nb">true</span> <span class="se">\</span> <span class="nt">-e</span> <span class="nv">DISABLE_SECURITY_PLUGIN</span><span class="o">=</span><span class="nb">true</span> <span class="se">\</span> <span class="nt">-v</span> <span class="si">$(</span><span class="nb">pwd</span><span class="si">)</span>/opensearch-data1:/usr/share/opensearch/data <span class="se">\</span> opensearchproject/opensearch:latest </code></pre></div></div> <p>We manually start it with <code class="language-plaintext highlighter-rouge">./opensearch-docker-entrypoint.sh</code>, then insert some data from another shell.</p> <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>curl <span class="nt">-X</span> POST http://localhost:9200/movies/_doc <span class="nt">--json</span> <span class="s1">'{"director":"Bennett Miller","title":"The Cruise","year":1998}'</span> </code></pre></div></div> <div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w"> </span><span class="nl">"_index"</span><span class="p">:</span><span class="w"> </span><span class="s2">"movies"</span><span class="p">,</span><span class="w"> </span><span class="nl">"_id"</span><span class="p">:</span><span class="w"> </span><span class="s2">"B0VvT5MBVIiutAAib7dO"</span><span class="p">,</span><span class="w"> </span><span class="nl">"_version"</span><span class="p">:</span><span class="w"> </span><span class="mi">1</span><span class="p">,</span><span class="w"> </span><span class="nl">"result"</span><span class="p">:</span><span class="w"> </span><span class="s2">"created"</span><span class="p">,</span><span class="w"> </span><span class="nl">"_shards"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nl">"total"</span><span class="p">:</span><span class="w"> </span><span class="mi">1</span><span class="p">,</span><span class="w"> </span><span class="nl">"successful"</span><span class="p">:</span><span class="w"> </span><span class="mi">1</span><span class="p">,</span><span class="w"> </span><span class="nl">"failed"</span><span class="p">:</span><span class="w"> </span><span class="mi">0</span><span class="w"> </span><span class="p">},</span><span class="w"> </span><span class="nl">"_seq_no"</span><span class="p">:</span><span class="w"> </span><span class="mi">2</span><span class="p">,</span><span class="w"> </span><span class="nl">"_primary_term"</span><span class="p">:</span><span class="w"> </span><span class="mi">2</span><span class="w"> </span><span class="p">}</span><span class="w"> </span></code></pre></div></div> <p>Remove the index replicas.</p> <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>curl <span class="nt">-X</span> PUT http://localhost:9200/movies/_settings <span class="nt">--json</span> <span class="s1">'{"index.number_of_replicas":0}'</span> </code></pre></div></div> <div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="nl">"acknowledged"</span><span class="p">:</span><span class="kc">true</span><span class="p">}</span><span class="w"> </span></code></pre></div></div> <p>Detach the cluster from the Docker shell.</p> <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>./bin/opensearch-node detach-cluster Confirm <span class="o">[</span>y/N] y Node was successfully detached from the cluster </code></pre></div></div> <p>Exit the container with <code class="language-plaintext highlighter-rouge">exit</code> and start a new 2-node cluster on top of this data with <a href="https://github.com/opensearch-project/opensearch-api-specification/blob/20b3b04dd6a998929e01e6494d4ac897ac36eaff/tests/dangling/.data/docker-compose.yml">this docker-compose.yml</a> that mounts our previously detached data.</p> <div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">services</span><span class="pi">:</span> <span class="na">opensearch-node1</span><span class="pi">:</span> <span class="s">...</span> <span class="s">volumes</span><span class="err">:</span> <span class="pi">-</span> <span class="s">./opensearch-data1:/usr/share/opensearch/data</span> <span class="na">ports</span><span class="pi">:</span> <span class="pi">-</span> <span class="s">9200:9200</span> <span class="pi">-</span> <span class="s">9600:9600</span> <span class="na">opensearch-node2</span><span class="pi">:</span> <span class="s">...</span> </code></pre></div></div> <p>After about 30 seconds the cluster figures out what to do, and creates the desired dangling indexes out of the existing data in <code class="language-plaintext highlighter-rouge">node1</code>.</p> <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>curl <span class="nt">-X</span> GET http://localhost:9200/_dangling </code></pre></div></div> <div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w"> </span><span class="nl">"_nodes"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nl">"total"</span><span class="p">:</span><span class="w"> </span><span class="mi">1</span><span class="p">,</span><span class="w"> </span><span class="nl">"successful"</span><span class="p">:</span><span class="w"> </span><span class="mi">1</span><span class="p">,</span><span class="w"> </span><span class="nl">"failed"</span><span class="p">:</span><span class="w"> </span><span class="mi">0</span><span class="w"> </span><span class="p">},</span><span class="w"> </span><span class="nl">"cluster_name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"docker-cluster"</span><span class="p">,</span><span class="w"> </span><span class="nl">"dangling_indices"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nl">"index_name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"movies"</span><span class="p">,</span><span class="w"> </span><span class="nl">"index_uuid"</span><span class="p">:</span><span class="w"> </span><span class="s2">"p6tliBznQO-FzTdslShrwA"</span><span class="p">,</span><span class="w"> </span><span class="nl">"creation_date_millis"</span><span class="p">:</span><span class="w"> </span><span class="mi">1732195242038</span><span class="p">,</span><span class="w"> </span><span class="nl">"node_ids"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w"> </span><span class="s2">"66ikk5pNQn-W9H0UN08OgA"</span><span class="w"> </span><span class="p">]</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="p">]</span><span class="w"> </span><span class="p">}</span><span class="w"> </span></code></pre></div></div> <p>Trying to recover this data will not work because the cluster is in a quorum loss and there’s no elected cluster manager.</p> <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>curl <span class="nt">-X</span> POST http://localhost:9200/_dangling/NAXLxxvEQfKHwRzxA7nxRw?accept_data_loss<span class="o">=</span><span class="nb">true</span> </code></pre></div></div> <div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w"> </span><span class="nl">"error"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nl">"root_cause"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nl">"type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"cluster_manager_not_discovered_exception"</span><span class="p">,</span><span class="w"> </span><span class="nl">"reason"</span><span class="p">:</span><span class="w"> </span><span class="s2">"no cluster-manager to send allocate dangled request"</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="p">],</span><span class="w"> </span><span class="nl">"type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"cluster_manager_not_discovered_exception"</span><span class="p">,</span><span class="w"> </span><span class="nl">"reason"</span><span class="p">:</span><span class="w"> </span><span class="s2">"no cluster-manager to send allocate dangled request"</span><span class="w"> </span><span class="p">},</span><span class="w"> </span><span class="nl">"status"</span><span class="p">:</span><span class="w"> </span><span class="mi">503</span><span class="w"> </span><span class="p">}</span><span class="w"> </span></code></pre></div></div> <p>Deleting the dangling index will also hang waiting for a cluster manager.</p> <p>We can turn our 2-node quorum loss cluster into a healthy single-node cluster with these dangling indexes. Attempting to switch the cluster <code class="language-plaintext highlighter-rouge">discovery.type=single-node</code> will not work because the node is part of a 2-node cluster that lost quorum.</p> <pre><code class="language-txt">opensearch-node1-1 | [2024-11-21T16:22:38,376][ERROR][o.o.b.OpenSearchUncaughtExceptionHandler] [opensearch-node1] uncaught exception in thread [main] opensearch-node1-1 | org.opensearch.bootstrap.StartupException: java.lang.IllegalStateException: cannot start with [discovery.type] set to [single-node] when local node {opensearch-node1}{UvfTjEerS76oc3RM5meQug}{8-SaRbGfSP64wLDkSwubNA}{172.19.0.2}{172.19.0.2:9300}{dimr}{shard_indexing_pressure_enabled=true} does not have quorum in voting configuration VotingConfiguration{_must_join_elected_cluster_manager_} </code></pre> <p>We need to properly detach one of the nodes. This is done by setting the number of replicas to zero, and removing node2 from voting.</p> <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>curl <span class="nt">-X</span> PUT http://localhost:9200/_settings <span class="nt">--json</span> <span class="s1">'{"index.number_of_replicas":0}'</span> curl <span class="nt">-X</span> POST http://localhost:9200/_cluster/voting_config_exclusions?node_names<span class="o">=</span>opensearch-node2 </code></pre></div></div> <p>A single node cluster can now start on top of this data.</p> <p>However, while we can mount it directly via docker-compose, we don’t want to update the local copy of our data files, and instead copy them into the Docker container every time in order to have a stable initial state. Copying files causes OpenSearch to think that they were tampered with externally.</p> <pre><code class="language-txt">opensearch-cluster-1 | [2024-11-21T15:02:27,832][ERROR][o.o.b.OpenSearchUncaughtExceptionHandler] [0e69ab6a4227] uncaught exception in thread [main] opensearch-cluster-1 | org.opensearch.bootstrap.StartupException: org.apache.lucene.store.AlreadyClosedException: Underlying file changed by an external force at 2024-11-21T15:02:27.801461006Z, (lock=NativeFSLock(path=/usr/share/opensearch/data/nodes/0/_state/write.lock,impl=sun.nio.ch.FileLockImpl[0:9223372036854775807 exclusive valid],creationTime=2024-11-21T15:02:21.011461002Z)) </code></pre> <p>The solution is to get rid of lock files with <code class="language-plaintext highlighter-rouge">find . -name *.lock | xargs rm</code>. Now our local data is usable in a single-node cluster and this is what we add to git. Finally, copied files also have to be writable. We add <code class="language-plaintext highlighter-rouge">COPY --chown=1000:1000 .data/opensearch-data1 /usr/share/opensearch/data</code> to <code class="language-plaintext highlighter-rouge">Dockerfile</code> and end up with a single-node cluster with dangling indexes that can be deleted or recovered.</p> <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>curl <span class="nt">-X</span> DELETE http://localhost:9200/_dangling/p6tliBznQO-FzTdslShrwA?accept_data_loss<span class="o">=</span><span class="nb">true</span> </code></pre></div></div> <div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="nl">"acknowledged"</span><span class="p">:</span><span class="kc">true</span><span class="p">}</span><span class="w"> </span></code></pre></div></div> <p>Note that <code class="language-plaintext highlighter-rouge">DELETE</code> and <code class="language-plaintext highlighter-rouge">POST _dangling</code> return <code class="language-plaintext highlighter-rouge">202 Accepted</code> and not <code class="language-plaintext highlighter-rouge">200</code> or <code class="language-plaintext highlighter-rouge">201</code>.</p> <p>See <a href="https://github.com/opensearch-project/opensearch-api-specification/pull/686">opensearch-api-specification#686</a> for details.</p> <p><a href="https://code.dblock.org/2024/11/21/creating-an-opensearch-cluster-with-dangling-indexes.html">Creating an OpenSearch Cluster with Dangling Indexes</a> was originally published by Daniel Doubrovkine at <a href="https://code.dblock.org">code.dblock.org | tech blog</a> on November 21, 2024.</p>