<?xml version="1.0" encoding="utf-8"?>
<?xml-stylesheet href="https://www.yellowduck.be/pretty-atom-feed-v3.xsl" type="text/xsl"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <title>🐥 YellowDuck.be</title>
  <id>https://www.yellowduck.be/posts/feed</id>
  <updated>2026-11-10T18:00:00Z</updated>
  <author>
    <name>Pieter Claerhout</name>
    <email>pieter@yellowduck.be</email>
  </author>
  <link rel="self" href="https://www.yellowduck.be/posts/feed"/>
  <link rel="alternate" href="https://www.yellowduck.be"/>
  <entry>
    <id>https://www.yellowduck.be/posts/passing-structs-with-a-rust-nif-in-elixir</id>
    <title>🐥 Passing Structs with a Rust NIF in Elixir</title>
    <updated>2026-11-10T18:00:00Z</updated>
    <published>2026-11-10T18:00:00Z</published>
    <author><name>Pieter Claerhout</name><email>pieter@yellowduck.be</email></author>
    <link rel="alternate" href="https://www.yellowduck.be/posts/passing-structs-with-a-rust-nif-in-elixir"/>
    <content type="html">&lt;p&gt;When using Rust NIFs in Elixir via the &lt;a href=&quot;https://github.com/rusterlium/rustler&quot;&gt;Rustler&lt;/a&gt; crate, it's often useful to pass structured data, not just integers and strings. This post shows how to pass an Elixir struct to Rust, manipulate it, and return a new struct to Elixir.&lt;/p&gt;
&lt;h1&gt;Define a struct in Elixir&lt;/h1&gt;
&lt;p&gt;Start by creating a simple struct, such as &lt;code&gt;User&lt;/code&gt;:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-elixir&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;span class=&quot;l-line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;l-keyword-function&quot;&gt;defmodule&lt;/span&gt; &lt;span class=&quot;l-module&quot;&gt;MyNif.User&lt;/span&gt; &lt;span class=&quot;l-keyword-function&quot;&gt;do&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;2&quot;&gt;  &lt;span class=&quot;l-keyword-function&quot;&gt;defstruct&lt;/span&gt; &lt;span class=&quot;l-punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;l-string-special-symbol&quot;&gt;:name&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-string-special-symbol&quot;&gt;:age&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;3&quot;&gt;&lt;span class=&quot;l-keyword&quot;&gt;end&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;Define the same struct in Rust&lt;/h1&gt;
&lt;p&gt;In your NIF crate (&lt;code&gt;native/my_nif/src/lib.rs&lt;/code&gt;), define a matching Rust struct using Rustler's &lt;code&gt;NifStruct&lt;/code&gt; derive macro:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-rust&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;span class=&quot;l-line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;l-keyword-import&quot;&gt;use&lt;/span&gt; &lt;span class=&quot;l-module&quot;&gt;rustler&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;l-type&quot;&gt;NifStruct&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;2&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;3&quot;&gt;&lt;span class=&quot;l-punctuation-special&quot;&gt;#&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;l-function-macro&quot;&gt;derive&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-type&quot;&gt;NifStruct&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;4&quot;&gt;&lt;span class=&quot;l-punctuation-special&quot;&gt;#&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;l-function-macro&quot;&gt;module&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;&amp;quot;MyNif.User&amp;quot;&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;5&quot;&gt;&lt;span class=&quot;l-keyword-type&quot;&gt;struct&lt;/span&gt; &lt;span class=&quot;l-type&quot;&gt;User&lt;/span&gt; &lt;span class=&quot;l-punctuation-bracket&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;6&quot;&gt;    &lt;span class=&quot;l-variable-member&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;l-type&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;7&quot;&gt;    &lt;span class=&quot;l-variable-member&quot;&gt;age&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;l-type-builtin&quot;&gt;u32&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;8&quot;&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;#[module = &amp;quot;MyNif.User&amp;quot;]&lt;/code&gt; attribute tells Rustler how to map the Elixir struct to the Rust struct.&lt;/p&gt;
&lt;h1&gt;Write a NIF function that uses the struct&lt;/h1&gt;
&lt;p&gt;Here's a Rust NIF that increases a user's age:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-rust&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;span class=&quot;l-line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;l-punctuation-special&quot;&gt;#&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;l-module&quot;&gt;rustler&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;l-function-macro&quot;&gt;nif&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;2&quot;&gt;&lt;span class=&quot;l-keyword-function&quot;&gt;fn&lt;/span&gt; &lt;span class=&quot;l-function&quot;&gt;birthday&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-variable-parameter&quot;&gt;user&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;l-type&quot;&gt;User&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;l-punctuation-delimiter&quot;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&quot;l-type&quot;&gt;User&lt;/span&gt; &lt;span class=&quot;l-punctuation-bracket&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;3&quot;&gt;    &lt;span class=&quot;l-type&quot;&gt;User&lt;/span&gt; &lt;span class=&quot;l-punctuation-bracket&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;4&quot;&gt;        &lt;span class=&quot;l-variable-member&quot;&gt;age&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;l-variable&quot;&gt;user&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;l-variable-member&quot;&gt;age&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;l-number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;5&quot;&gt;        &lt;span class=&quot;l-operator&quot;&gt;..&lt;/span&gt;&lt;span class=&quot;l-variable&quot;&gt;user&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;6&quot;&gt;    &lt;span class=&quot;l-punctuation-bracket&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;7&quot;&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Register the function:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-rust&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;span class=&quot;l-line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;l-module&quot;&gt;rustler&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;l-function-macro&quot;&gt;init!&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;&amp;quot;Elixir.MyNif&amp;quot;&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;Wire it up in Elixir&lt;/h1&gt;
&lt;p&gt;In your wrapper module:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-elixir&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;span class=&quot;l-line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;l-keyword-function&quot;&gt;defmodule&lt;/span&gt; &lt;span class=&quot;l-module&quot;&gt;MyNif&lt;/span&gt; &lt;span class=&quot;l-keyword-function&quot;&gt;do&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;2&quot;&gt;  &lt;span class=&quot;l-keyword&quot;&gt;use&lt;/span&gt; &lt;span class=&quot;l-module&quot;&gt;Rustler&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-string-special-symbol&quot;&gt;otp_app: :my_nif_app&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-string-special-symbol&quot;&gt;crate: &lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;&amp;quot;my_nif&amp;quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;3&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;4&quot;&gt;  &lt;span class=&quot;l-keyword-function&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;l-function&quot;&gt;birthday&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-comment&quot;&gt;_user&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-string-special-symbol&quot;&gt;do: &lt;/span&gt;&lt;span class=&quot;l-type&quot;&gt;:erlang&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;l-function-call&quot;&gt;nif_error&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-string-special-symbol&quot;&gt;:nif_not_loaded&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;5&quot;&gt;&lt;span class=&quot;l-keyword&quot;&gt;end&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;Call it from IEx&lt;/h1&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-elixir&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;span class=&quot;l-line&quot; data-line=&quot;1&quot;&gt;$ &lt;span class=&quot;l-function-call&quot;&gt;iex&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;l-module&quot;&gt;S&lt;/span&gt; mix&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;2&quot;&gt;iex&lt;span class=&quot;l-operator&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;l-variable&quot;&gt;user&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;l-punctuation-special&quot;&gt;%&lt;/span&gt;&lt;span class=&quot;l-module&quot;&gt;MyNif.User&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;l-string-special-symbol&quot;&gt;name: &lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;&amp;quot;Alice&amp;quot;&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-string-special-symbol&quot;&gt;age: &lt;/span&gt;&lt;span class=&quot;l-number&quot;&gt;29&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;3&quot;&gt;&lt;span class=&quot;l-variable&quot;&gt;iex&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;l-module&quot;&gt;MyNif&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;l-function-call&quot;&gt;birthday&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-variable&quot;&gt;user&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;4&quot;&gt;&lt;span class=&quot;l-punctuation-special&quot;&gt;%&lt;/span&gt;&lt;span class=&quot;l-module&quot;&gt;MyNif.User&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;l-string-special-symbol&quot;&gt;name: &lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;&amp;quot;Alice&amp;quot;&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-string-special-symbol&quot;&gt;age: &lt;/span&gt;&lt;span class=&quot;l-number&quot;&gt;30&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;Notes&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;Fields must match exactly between Elixir and Rust.&lt;/li&gt;
&lt;li&gt;Rustler will raise a badarg error if field names or types don't match.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;NifStruct&lt;/code&gt; automatically handles encoding and decoding using &lt;code&gt;serde&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;&lt;a href=&quot;https://www.yellowduck.be/tags/rust&quot;&gt;#rust&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/elixir&quot;&gt;#elixir&lt;/a&gt;&lt;/p&gt;</content>
  </entry>
  <entry>
    <id>https://www.yellowduck.be/posts/creating-a-simple-elixir-nif-using-rust</id>
    <title>🐥 Creating a Simple Elixir NIF Using Rust</title>
    <updated>2026-11-08T18:00:00Z</updated>
    <published>2026-11-08T18:00:00Z</published>
    <author><name>Pieter Claerhout</name><email>pieter@yellowduck.be</email></author>
    <link rel="alternate" href="https://www.yellowduck.be/posts/creating-a-simple-elixir-nif-using-rust"/>
    <content type="html">&lt;p&gt;Elixir is great for many things, but sometimes you need raw speed or access to native libraries. That’s where &lt;strong&gt;Native Implemented Functions (NIFs)&lt;/strong&gt; come in. This post walks through creating a simple Elixir NIF using Rust with the help of the &lt;a href=&quot;https://github.com/rusterlium/rustler&quot;&gt;&lt;code&gt;rustler&lt;/code&gt;&lt;/a&gt; crate.&lt;/p&gt;
&lt;h1&gt;Set up your Elixir project&lt;/h1&gt;
&lt;p&gt;Start with a basic Elixir application:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-bash&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;span class=&quot;l-line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;l-function-call&quot;&gt;mix&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;my_nif_app&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;2&quot;&gt;&lt;span class=&quot;l-function-builtin&quot;&gt;cd&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;my_nif_app&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Add &lt;code&gt;:rustler&lt;/code&gt; as a dependency in &lt;code&gt;mix.exs&lt;/code&gt;:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-elixir&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;span class=&quot;l-line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;l-keyword-function&quot;&gt;defp&lt;/span&gt; &lt;span class=&quot;l-function&quot;&gt;deps&lt;/span&gt; &lt;span class=&quot;l-keyword&quot;&gt;do&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;2&quot;&gt;  &lt;span class=&quot;l-punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;3&quot;&gt;    &lt;span class=&quot;l-punctuation-bracket&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;l-string-special-symbol&quot;&gt;:rustler&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;&amp;quot;~&amp;gt; 0.38.0&amp;quot;&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;4&quot;&gt;  &lt;span class=&quot;l-punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;5&quot;&gt;&lt;span class=&quot;l-keyword&quot;&gt;end&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Fetch the dependencies:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-bash&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;span class=&quot;l-line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;l-function-call&quot;&gt;mix&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;deps.get&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;Generate the Rust NIF scaffold&lt;/h1&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-bash&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;span class=&quot;l-line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;l-punctuation-special&quot;&gt;$&lt;/span&gt; &lt;span class=&quot;l-variable&quot;&gt;mix&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;rustler.new&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;2&quot;&gt;&lt;span class=&quot;l-function-call&quot;&gt;This&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;is&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;the&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;name&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;of&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;the&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;Elixir&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;module&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;the&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;NIF&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;module&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;will&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;be&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;registered&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;to.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;3&quot;&gt;&lt;span class=&quot;l-function-call&quot;&gt;Module&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;name&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;l-string-special-path&quot;&gt;MyNif&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;4&quot;&gt;&lt;span class=&quot;l-function-call&quot;&gt;This&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;is&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;the&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;name&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;used&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;the&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;generated&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;Rust&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;crate.&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;The&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;default&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;is&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;most&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;likely&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;fine.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;5&quot;&gt;&lt;span class=&quot;l-function-call&quot;&gt;Library&lt;/span&gt; name &lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-function-call&quot;&gt;mynif&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;l-string-special-path&quot;&gt;my_nif&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;6&quot;&gt;&lt;span class=&quot;l-function-call&quot;&gt;Fetched&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;latest&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;rustler&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;crate&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;version:&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;0.39.0&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;7&quot;&gt;&lt;span class=&quot;l-function-call&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;creating&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;native/my_nif/README.md&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;8&quot;&gt;&lt;span class=&quot;l-function-call&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;creating&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;native/my_nif/Cargo.toml&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;9&quot;&gt;&lt;span class=&quot;l-function-call&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;creating&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;native/my_nif/src/lib.rs&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;10&quot;&gt;&lt;span class=&quot;l-function-call&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;creating&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;Cargo.toml&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;11&quot;&gt;&lt;span class=&quot;l-function-call&quot;&gt;Updating&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;.gitignore&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;file&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;12&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;13&quot;&gt;&lt;span class=&quot;l-function-call&quot;&gt;Ready&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;to&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;go!&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;See&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;native/my_nif/README.md&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;further&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;instructions.&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This command will:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Create a &lt;code&gt;native/&lt;/code&gt; directory&lt;/li&gt;
&lt;li&gt;Scaffold a Rust crate&lt;/li&gt;
&lt;li&gt;Link it into your Elixir project&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Choose a crate name when prompted, such as &lt;code&gt;my_nif&lt;/code&gt;.&lt;/p&gt;
&lt;h1&gt;Write a simple NIF in Rust&lt;/h1&gt;
&lt;p&gt;Open the generated &lt;code&gt;native/my_nif/src/lib.rs&lt;/code&gt; file and replace its contents with:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-rust&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;span class=&quot;l-line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;l-punctuation-special&quot;&gt;#&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;l-module&quot;&gt;rustler&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;l-function-macro&quot;&gt;nif&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;2&quot;&gt;&lt;span class=&quot;l-keyword-function&quot;&gt;fn&lt;/span&gt; &lt;span class=&quot;l-function&quot;&gt;add&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-variable-parameter&quot;&gt;a&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;l-type-builtin&quot;&gt;i64&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;b&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;l-type-builtin&quot;&gt;i64&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;l-punctuation-delimiter&quot;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&quot;l-type-builtin&quot;&gt;i64&lt;/span&gt; &lt;span class=&quot;l-punctuation-bracket&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;3&quot;&gt;    &lt;span class=&quot;l-variable&quot;&gt;a&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;l-variable&quot;&gt;b&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;4&quot;&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;5&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;6&quot;&gt;&lt;span class=&quot;l-module&quot;&gt;rustler&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;l-function-macro&quot;&gt;init!&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;&amp;quot;Elixir.MyNif&amp;quot;&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This defines a basic &lt;code&gt;add/2&lt;/code&gt; function exposed to Elixir.&lt;/p&gt;
&lt;h1&gt;Define the Elixir wrapper&lt;/h1&gt;
&lt;p&gt;In &lt;code&gt;lib/my_nif.ex&lt;/code&gt;, wire up the NIF:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-elixir&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;span class=&quot;l-line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;l-keyword-function&quot;&gt;defmodule&lt;/span&gt; &lt;span class=&quot;l-module&quot;&gt;MyNif&lt;/span&gt; &lt;span class=&quot;l-keyword-function&quot;&gt;do&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;2&quot;&gt;  &lt;span class=&quot;l-keyword&quot;&gt;use&lt;/span&gt; &lt;span class=&quot;l-module&quot;&gt;Rustler&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-string-special-symbol&quot;&gt;otp_app: :my_nif_app&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-string-special-symbol&quot;&gt;crate: &lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;&amp;quot;my_nif&amp;quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;3&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;4&quot;&gt;  &lt;span class=&quot;l-comment&quot;&gt;# Fallback when the NIF is not loaded&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;5&quot;&gt;  &lt;span class=&quot;l-keyword-function&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;l-function&quot;&gt;add&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-comment&quot;&gt;_a&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-comment&quot;&gt;_b&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-string-special-symbol&quot;&gt;do: &lt;/span&gt;&lt;span class=&quot;l-type&quot;&gt;:erlang&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;l-function-call&quot;&gt;nif_error&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-string-special-symbol&quot;&gt;:nif_not_loaded&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;6&quot;&gt;&lt;span class=&quot;l-keyword&quot;&gt;end&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;Build and test&lt;/h1&gt;
&lt;p&gt;Compile the NIF:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-bash&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;span class=&quot;l-line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;l-function-call&quot;&gt;mix&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;compile&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then try it out in &lt;code&gt;iex&lt;/code&gt;:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-elixir&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;span class=&quot;l-line&quot; data-line=&quot;1&quot;&gt;$ &lt;span class=&quot;l-function-call&quot;&gt;iex&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;l-module&quot;&gt;S&lt;/span&gt; mix&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;2&quot;&gt;iex&lt;span class=&quot;l-operator&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;l-module&quot;&gt;MyNif&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;l-function-call&quot;&gt;add&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-number&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-number&quot;&gt;3&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;3&quot;&gt;&lt;span class=&quot;l-number&quot;&gt;5&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You've created a simple Elixir NIF using Rust. This lets you use Rust's performance and ecosystem while retaining Elixir's productivity.&lt;/p&gt;&lt;p&gt;&lt;a href=&quot;https://www.yellowduck.be/tags/rust&quot;&gt;#rust&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/elixir&quot;&gt;#elixir&lt;/a&gt;&lt;/p&gt;</content>
  </entry>
  <entry>
    <id>https://www.yellowduck.be/posts/giving-a-laravel-artisan-command-an-alias</id>
    <title>🐥 Giving a Laravel Artisan Command an Alias</title>
    <updated>2026-11-06T18:00:00Z</updated>
    <published>2026-11-06T18:00:00Z</published>
    <author><name>Pieter Claerhout</name><email>pieter@yellowduck.be</email></author>
    <link rel="alternate" href="https://www.yellowduck.be/posts/giving-a-laravel-artisan-command-an-alias"/>
    <content type="html">&lt;p&gt;Sooner or later, every codebase has a command whose name you can't remember. Say you have &lt;code&gt;reports:generate-monthly&lt;/code&gt;. Half the team types &lt;code&gt;reports:monthly&lt;/code&gt; from muscle memory and gets &amp;quot;Command not defined&amp;quot;. You don't want to rename the command because runbooks, scripts, and Slack history all reference the old name. So you give it a second name.&lt;/p&gt;
&lt;h1&gt;The short version&lt;/h1&gt;
&lt;p&gt;Aliases are a Symfony Console feature, and Artisan is built on Symfony Console. Laravel supports them in three ways.&lt;/p&gt;
&lt;h2&gt;1. The &lt;code&gt;$aliases&lt;/code&gt; property&lt;/h2&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-php&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;span class=&quot;l-line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;l-line&quot; data-line=&quot;2&quot;&gt;&lt;span class=&quot;l-keyword-type&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;l-type&quot;&gt;GenerateMonthlyReportCommand&lt;/span&gt; &lt;span class=&quot;l-keyword&quot;&gt;extends&lt;/span&gt; &lt;span class=&quot;l-type&quot;&gt;Command&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;3&quot;&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;4&quot;&gt;    &lt;span class=&quot;l-keyword-modifier&quot;&gt;protected&lt;/span&gt; &lt;span class=&quot;l-property&quot;&gt;$signature&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;&amp;#39;reports:generate-monthly {reportId}&amp;#39;&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;5&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;6&quot;&gt;    &lt;span class=&quot;l-keyword-modifier&quot;&gt;protected&lt;/span&gt; &lt;span class=&quot;l-property&quot;&gt;$description&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;&amp;#39;Generates the monthly report for the given id&amp;#39;&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;7&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;8&quot;&gt;    &lt;span class=&quot;l-keyword-modifier&quot;&gt;protected&lt;/span&gt; &lt;span class=&quot;l-property&quot;&gt;$aliases&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;l-punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;&amp;#39;reports:monthly&amp;#39;&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;9&quot;&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Laravel's &lt;code&gt;Command&lt;/code&gt; constructor reads this property and calls Symfony's &lt;code&gt;setAliases()&lt;/code&gt; for you. This is the approach we use.&lt;/p&gt;
&lt;h2&gt;2. The &lt;code&gt;#[Aliases]&lt;/code&gt; attribute&lt;/h2&gt;
&lt;p&gt;Laravel 13 also has class attributes for command metadata:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-php&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;span class=&quot;l-line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;l-line&quot; data-line=&quot;2&quot;&gt;&lt;span class=&quot;l-keyword-import&quot;&gt;use&lt;/span&gt; &lt;span class=&quot;l-module&quot;&gt;Illuminate&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;\&lt;/span&gt;&lt;span class=&quot;l-module&quot;&gt;Console&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;\&lt;/span&gt;&lt;span class=&quot;l-module&quot;&gt;Attributes&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;\&lt;/span&gt;&lt;span class=&quot;l-type&quot;&gt;Aliases&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;3&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;4&quot;&gt;&lt;span class=&quot;l-attribute&quot;&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;#[&lt;/span&gt;Aliases&lt;span class=&quot;l-punctuation-bracket&quot;&gt;([&lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;&amp;#39;reports:monthly&amp;#39;&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;])]&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;5&quot;&gt;&lt;span class=&quot;l-keyword-type&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;l-type&quot;&gt;GenerateMonthlyReportCommand&lt;/span&gt; &lt;span class=&quot;l-keyword&quot;&gt;extends&lt;/span&gt; &lt;span class=&quot;l-type&quot;&gt;Command&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;6&quot;&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;7&quot;&gt;    &lt;span class=&quot;l-comment&quot;&gt;// ...&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;8&quot;&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;#[Signature]&lt;/code&gt; attribute also accepts an &lt;code&gt;aliases&lt;/code&gt; argument, so the signature and its aliases can sit together. Use whichever style your commands already use.&lt;/p&gt;
&lt;h2&gt;3. Overriding &lt;code&gt;configure()&lt;/code&gt;&lt;/h2&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-php&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;span class=&quot;l-line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;l-line&quot; data-line=&quot;2&quot;&gt;&lt;span class=&quot;l-keyword-modifier&quot;&gt;protected&lt;/span&gt; &lt;span class=&quot;l-keyword-function&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;l-function&quot;&gt;configure&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;()&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;l-type-builtin&quot;&gt;void&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;3&quot;&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;4&quot;&gt;    &lt;span class=&quot;l-variable-builtin&quot;&gt;parent&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;l-function-call&quot;&gt;configure&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;()&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;5&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;6&quot;&gt;    &lt;span class=&quot;l-variable-builtin&quot;&gt;$this&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;l-function-method-call&quot;&gt;setAliases&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;([&lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;&amp;#39;reports:monthly&amp;#39;&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;])&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;7&quot;&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This works on any Laravel version because it uses plain Symfony. On recent versions, you rarely need it.&lt;/p&gt;
&lt;h1&gt;What you get&lt;/h1&gt;
&lt;p&gt;Both names run the same command with the same arguments and options:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-bash&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;span class=&quot;l-line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;l-function-call&quot;&gt;php&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;artisan&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;reports:generate-monthly&lt;/span&gt; &lt;span class=&quot;l-number&quot;&gt;123&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;2&quot;&gt;&lt;span class=&quot;l-function-call&quot;&gt;php&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;artisan&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;reports:monthly&lt;/span&gt; &lt;span class=&quot;l-number&quot;&gt;123&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;php artisan list&lt;/code&gt; shows the alias as its own entry and marks the canonical command with the alias in brackets, so people can see that the two names belong together:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-plaintext&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;span class=&quot;l-line&quot; data-line=&quot;1&quot;&gt;reports:monthly            Generates the monthly report for the given id&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;2&quot;&gt;reports:generate-monthly   [reports:monthly] Generates the monthly report for the given id&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;Gotchas&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;An alias is only a name.&lt;/strong&gt; Don't put arguments or options in it. The signature (&lt;code&gt;{reportId}&lt;/code&gt;) belongs to the command and is shared by every name. If you write &lt;code&gt;'reports:monthly {reportId}'&lt;/code&gt; as an alias, Symfony takes the whole string as the name, including the placeholder.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Aliases must be unique.&lt;/strong&gt; Run &lt;code&gt;php artisan list&lt;/code&gt; after adding one to check that it doesn't collide with another command.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Scheduling and queues.&lt;/strong&gt; &lt;code&gt;Schedule::command('reports:monthly')&lt;/code&gt;, &lt;code&gt;$this-&amp;gt;call()&lt;/code&gt;, and &lt;code&gt;Artisan::call()&lt;/code&gt; all resolve aliases. Still, use the canonical name in code so a grep for the command finds every use.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Decide which name is canonical.&lt;/strong&gt; The alias is a convenience. If you want people to move to the new name, make it the &lt;code&gt;$signature&lt;/code&gt; name and keep the old one as the alias. That gives you a painless rename.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Test it.&lt;/strong&gt; Run the command through its alias once in a test so a typo in the alias doesn't go unnoticed.&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;&lt;a href=&quot;https://www.yellowduck.be/tags/laravel&quot;&gt;#laravel&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/php&quot;&gt;#php&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/tools&quot;&gt;#tools&lt;/a&gt;&lt;/p&gt;</content>
  </entry>
  <entry>
    <id>https://www.yellowduck.be/posts/rsync-backups-that-respect-your-gitignore</id>
    <title>🐥 rsync backups that respect your .gitignore</title>
    <updated>2026-11-03T18:00:00Z</updated>
    <published>2026-11-03T18:00:00Z</published>
    <author><name>Pieter Claerhout</name><email>pieter@yellowduck.be</email></author>
    <link rel="alternate" href="https://www.yellowduck.be/posts/rsync-backups-that-respect-your-gitignore"/>
    <content type="html">&lt;p&gt;A naive &lt;code&gt;rsync&lt;/code&gt; backup copies everything — including the megabytes of compiled assets, cached responses, and third-party dependencies you never actually need to restore. Here is how to make rsync smarter, without maintaining a separate exclude list.&lt;/p&gt;
&lt;h1&gt;The problem with a bare rsync&lt;/h1&gt;
&lt;p&gt;The simplest backup command looks like this:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-bash&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;span class=&quot;l-line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;l-function-call&quot;&gt;rsync&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;--delete&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;-avh&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;/src/&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;/dst/&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It works, but it faithfully copies &lt;code&gt;node_modules/&lt;/code&gt; (often hundreds of megabytes), compiled caches, vendor trees, and log files that rotate anyway. These are not worth transferring, not worth storing, and not worth restoring.&lt;/p&gt;
&lt;h2&gt;Explicit excludes for the obvious offenders&lt;/h2&gt;
&lt;p&gt;The first fix is straightforward: pass &lt;code&gt;--exclude&lt;/code&gt; flags for the directories you know you never want.&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-bash&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;span class=&quot;l-line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;l-function-call&quot;&gt;rsync&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;--delete&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;-avh&lt;/span&gt; \&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;2&quot;&gt;  &lt;span class=&quot;l-variable-parameter&quot;&gt;--exclude=&lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;&amp;#39;logs/&amp;#39;&lt;/span&gt; \&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;3&quot;&gt;  &lt;span class=&quot;l-variable-parameter&quot;&gt;--exclude=&lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;&amp;#39;node_modules/&amp;#39;&lt;/span&gt; \&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;4&quot;&gt;  &lt;span class=&quot;l-variable-parameter&quot;&gt;--exclude=&lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;&amp;#39;vendor/&amp;#39;&lt;/span&gt; \&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;5&quot;&gt;  &lt;span class=&quot;l-variable-parameter&quot;&gt;--exclude=&lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;&amp;#39;cache/&amp;#39;&lt;/span&gt; \&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;6&quot;&gt;  &lt;span class=&quot;l-variable-parameter&quot;&gt;/src/&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;/dst/&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The trailing slash on each pattern matters: it matches directories specifically, not files that happen to share the name.&lt;/p&gt;
&lt;h1&gt;Honouring .gitignore automatically&lt;/h1&gt;
&lt;p&gt;The more interesting problem is everything your &lt;code&gt;.gitignore&lt;/code&gt; already documents but &lt;code&gt;rsync&lt;/code&gt; doesn't know about. Build outputs, compiled assets, secrets files — you've already told git to ignore them. Telling rsync separately is just duplication.&lt;/p&gt;
&lt;p&gt;rsync has a &lt;code&gt;--filter&lt;/code&gt; option that solves this cleanly:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-bash&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;span class=&quot;l-line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;l-comment&quot;&gt;# &amp;#39;:- .gitignore&amp;#39; means: merge each .gitignore as an exclude list&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;2&quot;&gt;&lt;span class=&quot;l-function-call&quot;&gt;rsync&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;--delete&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;-avh&lt;/span&gt; \&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;3&quot;&gt;  &lt;span class=&quot;l-variable-parameter&quot;&gt;--filter=&lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;&amp;#39;:- .gitignore&amp;#39;&lt;/span&gt; \&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;4&quot;&gt;  &lt;span class=&quot;l-variable-parameter&quot;&gt;--exclude=&lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;&amp;#39;logs/&amp;#39;&lt;/span&gt; \&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;5&quot;&gt;  &lt;span class=&quot;l-variable-parameter&quot;&gt;--exclude=&lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;&amp;#39;node_modules/&amp;#39;&lt;/span&gt; \&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;6&quot;&gt;  &lt;span class=&quot;l-variable-parameter&quot;&gt;--exclude=&lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;&amp;#39;vendor/&amp;#39;&lt;/span&gt; \&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;7&quot;&gt;  &lt;span class=&quot;l-variable-parameter&quot;&gt;--exclude=&lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;&amp;#39;cache/&amp;#39;&lt;/span&gt; \&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;8&quot;&gt;  &lt;span class=&quot;l-variable-parameter&quot;&gt;/src/&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;/dst/&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;:- .gitignore&lt;/code&gt; rule is a &lt;em&gt;dir-merge&lt;/em&gt; filter. As rsync descends into each directory, it reads that directory's &lt;code&gt;.gitignore&lt;/code&gt; and applies its patterns as excludes — recursively, the same way git itself does.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; The &lt;code&gt;--filter&lt;/code&gt; flag reads the &lt;em&gt;contents&lt;/em&gt; of &lt;code&gt;.gitignore&lt;/code&gt; files, not the &lt;code&gt;.git/&lt;/code&gt; directory itself. Git's implicit exclusion of &lt;code&gt;.git/&lt;/code&gt; is not part of any &lt;code&gt;.gitignore&lt;/code&gt;, so those folders &lt;strong&gt;are&lt;/strong&gt; included in the backup. Add &lt;code&gt;--exclude='.git/'&lt;/code&gt; if you want to omit them.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h1&gt;Why keep the explicit excludes too?&lt;/h1&gt;
&lt;p&gt;&lt;code&gt;node_modules/&lt;/code&gt; and friends are almost always in &lt;code&gt;.gitignore&lt;/code&gt; already, so in practice the explicit flags are redundant for most projects. But they act as a safety net: if you're backing up a directory that has no &lt;code&gt;.gitignore&lt;/code&gt;, or an older project where someone forgot to gitignore the vendor folder, the explicit rules still fire. Belt and braces.&lt;/p&gt;
&lt;h1&gt;When --delete can't clean up after itself&lt;/h1&gt;
&lt;p&gt;Once you add &lt;code&gt;--filter=':- .gitignore'&lt;/code&gt;, you may see a warning like this:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-plaintext&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;span class=&quot;l-line&quot; data-line=&quot;1&quot;&gt;rsync: not empty, cannot delete: ./some-project/.git&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;2&quot;&gt;rsync: not empty, cannot delete: ./some-project&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This happens when rsync's &lt;code&gt;--delete&lt;/code&gt; flag tries to remove a directory from the destination that no longer exists (or is now excluded) on the source — but can't fully empty it first. The cause is the exclude rules themselves: rsync won't touch files it has been told to skip, so if a directory contains excluded files, rsync leaves those in place, and then can't remove the non-empty parent.&lt;/p&gt;
&lt;p&gt;The fix is &lt;code&gt;--delete-excluded&lt;/code&gt;, which tells rsync to also delete destination files that match an exclude rule:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-bash&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;span class=&quot;l-line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;l-function-call&quot;&gt;rsync&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;--delete&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;--delete-excluded&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;-avh&lt;/span&gt; \&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;2&quot;&gt;  &lt;span class=&quot;l-variable-parameter&quot;&gt;--filter=&lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;&amp;#39;:- .gitignore&amp;#39;&lt;/span&gt; \&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;3&quot;&gt;  &lt;span class=&quot;l-variable-parameter&quot;&gt;--exclude=&lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;&amp;#39;logs/&amp;#39;&lt;/span&gt; \&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;4&quot;&gt;  &lt;span class=&quot;l-variable-parameter&quot;&gt;--exclude=&lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;&amp;#39;node_modules/&amp;#39;&lt;/span&gt; \&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;5&quot;&gt;  &lt;span class=&quot;l-variable-parameter&quot;&gt;--exclude=&lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;&amp;#39;vendor/&amp;#39;&lt;/span&gt; \&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;6&quot;&gt;  &lt;span class=&quot;l-variable-parameter&quot;&gt;--exclude=&lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;&amp;#39;cache/&amp;#39;&lt;/span&gt; \&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;7&quot;&gt;  &lt;span class=&quot;l-variable-parameter&quot;&gt;/src/&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;/dst/&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Worth knowing:&lt;/strong&gt; &lt;code&gt;--delete-excluded&lt;/code&gt; cuts both ways. If you add a pattern to a &lt;code&gt;.gitignore&lt;/code&gt; after a directory has already been backed up, the next run will remove those files from the destination too. For a mirror backup that should faithfully reflect the source, that is exactly the right behaviour — but it is worth being aware of.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h1&gt;The final script&lt;/h1&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-bash&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;span class=&quot;l-line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;l-keyword-directive&quot;&gt;#!/bin/bash&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;2&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;3&quot;&gt;&lt;span class=&quot;l-function-call&quot;&gt;rsync&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;--delete&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;--delete-excluded&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;-avh&lt;/span&gt; \&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;4&quot;&gt;  &lt;span class=&quot;l-variable-parameter&quot;&gt;--filter=&lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;&amp;#39;:- .gitignore&amp;#39;&lt;/span&gt; \&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;5&quot;&gt;  &lt;span class=&quot;l-variable-parameter&quot;&gt;--exclude=&lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;&amp;#39;logs/&amp;#39;&lt;/span&gt; \&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;6&quot;&gt;  &lt;span class=&quot;l-variable-parameter&quot;&gt;--exclude=&lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;&amp;#39;node_modules/&amp;#39;&lt;/span&gt; \&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;7&quot;&gt;  &lt;span class=&quot;l-variable-parameter&quot;&gt;--exclude=&lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;&amp;#39;vendor/&amp;#39;&lt;/span&gt; \&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;8&quot;&gt;  &lt;span class=&quot;l-variable-parameter&quot;&gt;--exclude=&lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;&amp;#39;cache/&amp;#39;&lt;/span&gt; \&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;9&quot;&gt;  &lt;span class=&quot;l-string&quot;&gt;&amp;quot;/path/to/source/&amp;quot;&lt;/span&gt; \&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;10&quot;&gt;  &lt;span class=&quot;l-string&quot;&gt;&amp;quot;/path/to/destination/&amp;quot;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Two lines of flags, one principle: let the projects describe what's ignorable, and make the backup command listen.&lt;/p&gt;&lt;p&gt;&lt;a href=&quot;https://www.yellowduck.be/tags/tools&quot;&gt;#tools&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/best-practice&quot;&gt;#best-practice&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/devops&quot;&gt;#devops&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/git&quot;&gt;#git&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/linux&quot;&gt;#linux&lt;/a&gt;&lt;/p&gt;</content>
  </entry>
  <entry>
    <id>https://www.yellowduck.be/posts/ini-set-memory-limit-now-throws-in-php-8-5</id>
    <title>🐥 ini_set('memory_limit') Now Throws in PHP 8.5</title>
    <updated>2026-10-29T18:00:00Z</updated>
    <published>2026-10-29T18:00:00Z</published>
    <author><name>Pieter Claerhout</name><email>pieter@yellowduck.be</email></author>
    <link rel="alternate" href="https://www.yellowduck.be/posts/ini-set-memory-limit-now-throws-in-php-8-5"/>
    <content type="html">&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; PHP 8.5 changed &lt;code&gt;ini_set('memory_limit', ...)&lt;/code&gt; to throw an &lt;code&gt;ErrorException&lt;/code&gt; when the requested value exceeds &lt;code&gt;max_memory_limit&lt;/code&gt;. Previously it silently capped. The fix is either to lower your requested limit, configure &lt;code&gt;max_memory_limit&lt;/code&gt; at the server level, or suppress the error when the cap is acceptable.&lt;/p&gt;
&lt;h1&gt;What happened&lt;/h1&gt;
&lt;p&gt;We upgraded a Laravel application to PHP 8.5 and a long-running Artisan import command immediately started failing with a cryptic error:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-plaintext&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;span class=&quot;l-line&quot; data-line=&quot;1&quot;&gt;ErrorException: Failed to set memory_limit to 4294967296 bytes.&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;2&quot;&gt;Setting to max_memory_limit instead (currently: 536870912 bytes)&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;3&quot;&gt;  File &amp;quot;app/Console/Commands/ImportDataCommand.php&amp;quot;, line 12&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;4&quot;&gt;    ini_set(&amp;#39;memory_limit&amp;#39;, &amp;#39;4096M&amp;#39;);&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The command was requesting 4 GB of memory. The server's hard ceiling (&lt;code&gt;max_memory_limit&lt;/code&gt;) is 512 MB. On PHP 8.4 that call silently set the limit to 512 MB and kept going. On PHP 8.5 it throws.&lt;/p&gt;
&lt;h1&gt;The PHP 8.5 change&lt;/h1&gt;
&lt;p&gt;PHP has two memory-related INI settings:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;memory_limit&lt;/code&gt;&lt;/strong&gt; — The per-request limit a script can raise or lower at runtime via &lt;code&gt;ini_set()&lt;/code&gt;. Typically set in &lt;code&gt;php.ini&lt;/code&gt; or a per-vhost config.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;max_memory_limit&lt;/code&gt;&lt;/strong&gt; — A hard ceiling added in PHP 8.5. Only changeable at the system level (php.ini, not at runtime). Scripts cannot exceed it.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Before 8.5, setting &lt;code&gt;memory_limit&lt;/code&gt; above the OS-level ceiling either worked (no hard enforcement) or was silently clamped. PHP 8.5 introduced &lt;code&gt;max_memory_limit&lt;/code&gt; as a formal concept and made the runtime enforce it with an error — because silently ignoring the requested value is surprising and hard to debug.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; This is a &lt;strong&gt;silent-to-loud&lt;/strong&gt; change. Code that appeared to work fine on 8.4 — because the limit was quietly capped — will now throw on 8.5 if the cap is exceeded.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h1&gt;The three real fixes&lt;/h1&gt;
&lt;h2&gt;1. Raise &lt;code&gt;max_memory_limit&lt;/code&gt; on the server (correct, if you own the server)&lt;/h2&gt;
&lt;p&gt;If the command genuinely needs 4 GB, the honest fix is to grant it at the system level. Add this to your &lt;code&gt;php.ini&lt;/code&gt; or CLI php.ini:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-ini&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;span class=&quot;l-line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;l-property&quot;&gt;max_memory_limit&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt; 4096M&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;2&quot;&gt;&lt;span class=&quot;l-property&quot;&gt;memory_limit&lt;/span&gt;     &lt;span class=&quot;l-operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt; 4096M&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then remove the &lt;code&gt;ini_set&lt;/code&gt; from the command entirely. Runtime limits belong in configuration, not in application code.&lt;/p&gt;
&lt;h2&gt;2. Pass the limit via the CLI (good for scheduled commands)&lt;/h2&gt;
&lt;p&gt;If you run the command from a scheduler or cron, set the limit as a CLI flag rather than from inside the script:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-bash&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;span class=&quot;l-line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;l-function-call&quot;&gt;php&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;-d&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;memory_limit=4096M&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;artisan&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;import:data&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This keeps the application code clean and makes the resource budget visible at the call site — right where your ops team or scheduler can see and change it.&lt;/p&gt;
&lt;h2&gt;3. Suppress the error and accept the cap (quick, pragmatic)&lt;/h2&gt;
&lt;p&gt;If the command works fine within the system's 512 MB ceiling, suppress the error with &lt;code&gt;@&lt;/code&gt;:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-php&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;span class=&quot;l-line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;l-line&quot; data-line=&quot;2&quot;&gt;&lt;span class=&quot;l-comment&quot;&gt;// Requests 4096M, silently accepts the max if lower&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;3&quot;&gt;&lt;span class=&quot;l-operator&quot;&gt;@&lt;/span&gt;&lt;span class=&quot;l-function-call&quot;&gt;ini_set&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;&amp;#39;memory_limit&amp;#39;&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;&amp;#39;4096M&amp;#39;&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Warning:&lt;/strong&gt; Only use this if you are genuinely okay with whatever the server's ceiling is. If the command &lt;em&gt;actually needs&lt;/em&gt; 4 GB and only gets 512 MB, you have suppressed a real warning and set yourself up for an OOM crash later.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;4. Use &lt;code&gt;-1&lt;/code&gt; (unlimited)&lt;/h2&gt;
&lt;p&gt;Passing &lt;code&gt;-1&lt;/code&gt; removes the PHP-level cap entirely and relies on the OS to manage memory. This does not trigger the &lt;code&gt;max_memory_limit&lt;/code&gt; check:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-php&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;span class=&quot;l-line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;l-line&quot; data-line=&quot;2&quot;&gt;&lt;span class=&quot;l-function-call&quot;&gt;ini_set&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;&amp;#39;memory_limit&amp;#39;&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;&amp;#39;-1&amp;#39;&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Warning:&lt;/strong&gt; Unlimited memory is a footgun on shared hosts or containers with soft limits. Use it only for trusted CLI commands on dedicated infrastructure.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h1&gt;What we did&lt;/h1&gt;
&lt;p&gt;The command runs as a scheduled job on a dedicated server. We moved the memory configuration out of the PHP script and into the cron invocation:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-bash&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;span class=&quot;l-line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;l-comment&quot;&gt;# Scheduler entry (before)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;2&quot;&gt;&lt;span class=&quot;l-function-call&quot;&gt;php&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;artisan&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;import:data&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;3&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;4&quot;&gt;&lt;span class=&quot;l-comment&quot;&gt;# Scheduler entry (after)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;5&quot;&gt;&lt;span class=&quot;l-function-call&quot;&gt;php&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;-d&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;memory_limit=4096M&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;artisan&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;import:data&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And removed the &lt;code&gt;ini_set&lt;/code&gt; from the command's &lt;code&gt;handle()&lt;/code&gt; method entirely. The command no longer makes assumptions about its own resource budget — that's the scheduler's job.&lt;/p&gt;
&lt;h1&gt;How to find this in your own codebase&lt;/h1&gt;
&lt;p&gt;Before upgrading to PHP 8.5, grep your codebase for runtime memory overrides:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-bash&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;span class=&quot;l-line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;l-function-call&quot;&gt;grep&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;-rn&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;&amp;quot;ini_set.*memory_limit&amp;quot;&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;app/&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;bootstrap/&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Any hit that requests more than your server's &lt;code&gt;max_memory_limit&lt;/code&gt; will throw. Check what your server's ceiling is with:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-bash&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;span class=&quot;l-line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;l-function-call&quot;&gt;php&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;-r&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;&amp;quot;echo ini_get(&amp;#39;max_memory_limit&amp;#39;);&amp;quot;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If that returns empty or &lt;code&gt;-1&lt;/code&gt;, no ceiling is currently configured and you are fine until an admin adds one.&lt;/p&gt;
&lt;p&gt;The &lt;a href=&quot;https://www.php.net/releases/8.5/en.php&quot;&gt;PHP 8.5 release notes&lt;/a&gt; cover this change under the INI settings section. The RFC that introduced &lt;code&gt;max_memory_limit&lt;/code&gt; is &lt;a href=&quot;https://wiki.php.net/rfc/max_memory_limit&quot;&gt;wiki.php.net/rfc/max_memory_limit&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;&lt;a href=&quot;https://www.yellowduck.be/tags/development&quot;&gt;#development&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/laravel&quot;&gt;#laravel&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/php&quot;&gt;#php&lt;/a&gt;&lt;/p&gt;</content>
  </entry>
  <entry>
    <id>https://www.yellowduck.be/posts/smarter-pr-size-labels-with-weighted-scoring</id>
    <title>🐥 Smarter PR Size Labels with Weighted Scoring</title>
    <updated>2026-10-27T18:00:00Z</updated>
    <published>2026-10-27T18:00:00Z</published>
    <author><name>Pieter Claerhout</name><email>pieter@yellowduck.be</email></author>
    <link rel="alternate" href="https://www.yellowduck.be/posts/smarter-pr-size-labels-with-weighted-scoring"/>
    <content type="html">&lt;p&gt;At work, we do a lot of code review. Like most teams, we've learned the hard way that a 1,500-line PR is not the same as a 500-line PR—even if the diff viewer disagrees.&lt;/p&gt;
&lt;p&gt;We've been running a GitHub Actions workflow that automatically labels every pull request with a size badge: &lt;code&gt;small&lt;/code&gt;, &lt;code&gt;medium&lt;/code&gt;, or &lt;code&gt;large&lt;/code&gt;. Recently, we overhauled it, and the improvements are worth sharing.&lt;/p&gt;
&lt;h1&gt;Why bother labelling PR size?&lt;/h1&gt;
&lt;p&gt;A size label on a PR does one simple thing: it sets expectations before anyone opens the diff.&lt;/p&gt;
&lt;p&gt;A reviewer who sees &lt;code&gt;small&lt;/code&gt; knows they can knock it out between two meetings. Someone who sees &lt;code&gt;large&lt;/code&gt; knows to carve out an hour and bring coffee. And if the whole team can see the size distribution in the pull request list at a glance, they can prioritise review queues more effectively and spot when a feature branch is growing out of hand before it becomes a problem.&lt;/p&gt;
&lt;p&gt;It also creates a gentle forcing function for authors. When you know the workflow is going to slap a &lt;code&gt;large&lt;/code&gt; on your PR, you think twice about whether those refactors really need to live in the same commit.&lt;/p&gt;
&lt;h1&gt;The naive approach and its problems&lt;/h1&gt;
&lt;p&gt;The first version of our workflow was simple: count additions, count deletions, and compare the total to a threshold.&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-plaintext&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;span class=&quot;l-line&quot; data-line=&quot;1&quot;&gt;score = additions + deletions&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That works, but it has some obvious blind spots:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Deleting code is easy to review.&lt;/strong&gt; A file removal or a big &lt;code&gt;git rm&lt;/code&gt; shouldn't count the same as a 200-line feature addition. The reviewer isn't absorbing new logic; they're confirming that something is gone.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Test files are verbose by design.&lt;/strong&gt; A well-tested change might add 300 lines of test code alongside 80 lines of feature code. Penalising it the same as 380 lines of business logic discourages good testing habits.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Some PRs are simply not reviewable.&lt;/strong&gt; Our old workflow topped out at &lt;code&gt;large&lt;/code&gt; regardless of score. A 5,000-line PR got the same label as a 700-line one, which gave reviewers no signal that they were looking at something that genuinely needed to be split.&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;The weighted scoring model&lt;/h1&gt;
&lt;p&gt;Our new approach scores each file individually, with different weights depending on the type of change.&lt;/p&gt;
&lt;h2&gt;Three file categories&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Deleted files&lt;/strong&gt; (&lt;code&gt;status === 'removed'&lt;/code&gt;): the entire file is going away. There's almost no cognitive load for a reviewer—they need to confirm the deletion is intentional and check for stray references. We weight the lines at 20% and add a flat 10 to the score per file.&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-javascript&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;span class=&quot;l-line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;l-variable&quot;&gt;score&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;+=&lt;/span&gt; &lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-variable&quot;&gt;f&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;l-variable-member&quot;&gt;additions&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;l-variable&quot;&gt;f&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;l-variable-member&quot;&gt;deletions&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;l-number&quot;&gt;0.2&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;l-number&quot;&gt;10&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Test files&lt;/strong&gt;: identified by a set of patterns covering the conventions we actually use (&lt;code&gt;*Test.php&lt;/code&gt;, &lt;code&gt;*Tests.php&lt;/code&gt;, &lt;code&gt;*.spec.ts&lt;/code&gt;, &lt;code&gt;*.test.ts&lt;/code&gt;, and paths under &lt;code&gt;tests/&lt;/code&gt; or &lt;code&gt;test/&lt;/code&gt;). These are important, but they're also repetitive—a reviewer can skim a block of assertions much faster than an equivalent block of business logic. Additions are weighted at 50%, deletions at 15%, and each test file adds 10 to the flat score.&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-javascript&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;span class=&quot;l-line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;l-variable&quot;&gt;score&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;+=&lt;/span&gt; &lt;span class=&quot;l-variable&quot;&gt;f&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;l-variable-member&quot;&gt;additions&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;l-number&quot;&gt;0.5&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;l-variable&quot;&gt;f&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;l-variable-member&quot;&gt;deletions&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;l-number&quot;&gt;0.15&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;l-number&quot;&gt;10&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Everything else&lt;/strong&gt;: regular source files count additions at full weight, deletions at 30%, and each file adds 20 to the flat score. The per-file component matters because touching ten files in different parts of the system—even with few lines changed—still takes real review time to understand the blast radius.&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-javascript&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;span class=&quot;l-line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;l-variable&quot;&gt;score&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;+=&lt;/span&gt; &lt;span class=&quot;l-variable&quot;&gt;f&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;l-variable-member&quot;&gt;additions&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;l-variable&quot;&gt;f&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;l-variable-member&quot;&gt;deletions&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;l-number&quot;&gt;0.3&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;l-number&quot;&gt;20&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Thresholds&lt;/h2&gt;
&lt;p&gt;Once we have the weighted score, we bucket it:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Label&lt;/th&gt;
&lt;th&gt;Score&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;small&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;≤ 250&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;medium&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;≤ 900&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;large&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;≤ 1800&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;not-reviewable&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&amp;gt; 1800&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;We deliberately raised the thresholds compared to our first version. The old numbers were too aggressive—a well-structured change with a handful of files and some tests would land in &lt;code&gt;medium&lt;/code&gt; even when it was genuinely easy to review. The new thresholds better reflect the actual reviewer experience.&lt;/p&gt;
&lt;h2&gt;The &lt;code&gt;not-reviewable&lt;/code&gt; label&lt;/h2&gt;
&lt;p&gt;This is new, and it's probably the most useful addition. When a PR scores above 1,800, it gets labelled &lt;code&gt;not-reviewable&lt;/code&gt;. That's a signal to the author—not a judgment, just a fact—that the PR is too large to give a meaningful review.&lt;/p&gt;
&lt;p&gt;It's also a signal to reviewers that they're not expected to wade through it; the author needs to split it first.&lt;/p&gt;
&lt;p&gt;The label swaps in and out cleanly alongside the others: if a PR starts as &lt;code&gt;not-reviewable&lt;/code&gt;, gets split, and the score drops, the next push removes the label and applies the correct one.&lt;/p&gt;
&lt;h1&gt;Ignoring noise&lt;/h1&gt;
&lt;p&gt;The workflow reads a &lt;code&gt;.github/pr-size-ignore.txt&lt;/code&gt; file where you can list glob patterns for files that should be excluded entirely from scoring. Ours includes things like lock files, generated assets, and vendored code. There's no point penalising a PR because Composer updated a JSON file.&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-plaintext&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;span class=&quot;l-line&quot; data-line=&quot;1&quot;&gt;# .github/pr-size-ignore.txt&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;2&quot;&gt;composer.lock&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;3&quot;&gt;package-lock.json&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;4&quot;&gt;*.min.js&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;5&quot;&gt;public/build/**&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;The full workflow&lt;/h1&gt;
&lt;p&gt;The workflow triggers on &lt;code&gt;pull_request_target&lt;/code&gt; (opened, synchronised, or reopened), skips Dependabot PRs, and needs &lt;code&gt;pull-requests: write&lt;/code&gt; and &lt;code&gt;issues: write&lt;/code&gt; permissions to manage labels.&lt;/p&gt;
&lt;p&gt;It paginates through the file list (GitHub caps the API at 3,000 files), filters out ignored paths, scores each file with the logic above, determines the label, removes any stale size label, and applies the new one. The whole thing runs in a single &lt;code&gt;actions/github-script&lt;/code&gt; step with no external action dependencies beyond &lt;code&gt;actions/checkout&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The label swap is idempotent: if the correct label is already on the PR, no API call is made.&lt;/p&gt;
&lt;h1&gt;Full workflow source&lt;/h1&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-yaml&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;span class=&quot;l-line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;l-property&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;PR Size Label&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;2&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;3&quot;&gt;&lt;span class=&quot;l-property&quot;&gt;on&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;4&quot;&gt;  &lt;span class=&quot;l-property&quot;&gt;pull_request_target&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;5&quot;&gt;    &lt;span class=&quot;l-property&quot;&gt;types&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;l-punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;opened&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;synchronize&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;reopened&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;6&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;7&quot;&gt;&lt;span class=&quot;l-property&quot;&gt;permissions&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;8&quot;&gt;  &lt;span class=&quot;l-property&quot;&gt;contents&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;read&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;9&quot;&gt;  &lt;span class=&quot;l-property&quot;&gt;pull-requests&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;write&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;10&quot;&gt;  &lt;span class=&quot;l-property&quot;&gt;issues&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;write&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;11&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;12&quot;&gt;&lt;span class=&quot;l-property&quot;&gt;jobs&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;13&quot;&gt;  &lt;span class=&quot;l-property&quot;&gt;label&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;14&quot;&gt;    &lt;span class=&quot;l-property&quot;&gt;runs-on&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;ubuntu-latest&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;15&quot;&gt;    &lt;span class=&quot;l-property&quot;&gt;if&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;github.actor != &amp;#39;dependabot[bot]&amp;#39;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;16&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;17&quot;&gt;    &lt;span class=&quot;l-property&quot;&gt;steps&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;18&quot;&gt;      &lt;span class=&quot;l-punctuation-delimiter&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;l-property&quot;&gt;uses&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;actions/checkout@v4&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;19&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;20&quot;&gt;      &lt;span class=&quot;l-punctuation-delimiter&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;l-property&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;Apply size label&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;21&quot;&gt;        &lt;span class=&quot;l-property&quot;&gt;uses&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;actions/github-script@v7&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;22&quot;&gt;        &lt;span class=&quot;l-property&quot;&gt;with&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;23&quot;&gt;          &lt;span class=&quot;l-property&quot;&gt;script&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;|&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;24&quot;&gt;&lt;span class=&quot;l-string&quot;&gt;            &lt;span class=&quot;l-function-call&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;fs&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;require&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;&amp;#39;fs&amp;#39;&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;25&quot;&gt;&lt;span class=&quot;l-string&quot;&gt;            &lt;span class=&quot;l-function-call&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;path&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;require&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;&amp;#39;path&amp;#39;&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;26&quot;&gt;&lt;span class=&quot;l-string&quot;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;27&quot;&gt;&lt;span class=&quot;l-string&quot;&gt;            &lt;span class=&quot;l-function-call&quot;&gt;//&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;Read&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;ignore&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;patterns&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;.github/pr-size-ignore.txt&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;28&quot;&gt;&lt;span class=&quot;l-string&quot;&gt;            &lt;span class=&quot;l-function-call&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;ignoreFile&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;&amp;#39;.github/pr-size-ignore.txt&amp;#39;&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;29&quot;&gt;&lt;span class=&quot;l-string&quot;&gt;            &lt;span class=&quot;l-function-call&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;ignorePatterns&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;fs.existsSync&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-function-call&quot;&gt;ignoreFile&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;30&quot;&gt;&lt;span class=&quot;l-string&quot;&gt;              &lt;span class=&quot;l-function-call&quot;&gt;?&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;fs.readFileSync&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-function-call&quot;&gt;ignoreFile,&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;&amp;#39;utf8&amp;#39;&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;31&quot;&gt;&lt;span class=&quot;l-string&quot;&gt;                  &lt;span class=&quot;l-function-call&quot;&gt;.split&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;&amp;#39;\n&amp;#39;&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;32&quot;&gt;&lt;span class=&quot;l-string&quot;&gt;                  &lt;span class=&quot;l-function-call&quot;&gt;.map&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-function-call&quot;&gt;l&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;l-string-special-path&quot;&gt;l.trim&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;())&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;33&quot;&gt;&lt;span class=&quot;l-string&quot;&gt;                  &lt;span class=&quot;l-function-call&quot;&gt;.filter&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-function-call&quot;&gt;l&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;l-string-special-path&quot;&gt;l&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class=&quot;l-function-call&quot;&gt;!l.startsWith&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;&amp;#39;#&amp;#39;&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;))&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;34&quot;&gt;&lt;span class=&quot;l-string&quot;&gt;              &lt;span class=&quot;l-function-builtin&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;[]&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;35&quot;&gt;&lt;span class=&quot;l-string&quot;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;36&quot;&gt;&lt;span class=&quot;l-string&quot;&gt;            &lt;span class=&quot;l-keyword-function&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;l-function&quot;&gt;globToRegex&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-function-call&quot;&gt;pattern&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;l-punctuation-bracket&quot;&gt;{&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;37&quot;&gt;&lt;span class=&quot;l-string&quot;&gt;              &lt;span class=&quot;l-function-builtin&quot;&gt;let&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;result&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;&amp;#39;&amp;#39;&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;38&quot;&gt;&lt;span class=&quot;l-string&quot;&gt;              &lt;span class=&quot;l-keyword-repeat&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-function-builtin&quot;&gt;let&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;i&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;l-number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;l-function-call&quot;&gt;i&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;l-string-special-path&quot;&gt;pattern.length&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;l-function-call&quot;&gt;i++&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;39&quot;&gt;&lt;span class=&quot;l-string&quot;&gt;                &lt;span class=&quot;l-function-call&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;c&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;pattern[i]&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;40&quot;&gt;&lt;span class=&quot;l-string&quot;&gt;                &lt;span class=&quot;l-keyword-conditional&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-function-call&quot;&gt;c&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;===&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;&amp;#39;*&amp;#39;&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class=&quot;l-variable&quot;&gt;pattern&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;[&lt;/span&gt;i &lt;span class=&quot;l-operator&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;l-number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;&amp;#39;*&amp;#39;&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;l-punctuation-bracket&quot;&gt;{&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;41&quot;&gt;&lt;span class=&quot;l-string&quot;&gt;                  &lt;span class=&quot;l-function-call&quot;&gt;result&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;+=&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;&amp;#39;.*&amp;#39;&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;42&quot;&gt;&lt;span class=&quot;l-string&quot;&gt;                  &lt;span class=&quot;l-function-call&quot;&gt;i++&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;43&quot;&gt;&lt;span class=&quot;l-string&quot;&gt;                  &lt;span class=&quot;l-keyword-conditional&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-variable&quot;&gt;pattern&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;[&lt;/span&gt;i &lt;span class=&quot;l-operator&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;l-number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;&amp;#39;/&amp;#39;&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt; i++&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;44&quot;&gt;&lt;span class=&quot;l-string&quot;&gt;                &lt;span class=&quot;l-function-call&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;else&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-function-call&quot;&gt;c&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;===&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;&amp;#39;*&amp;#39;&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;45&quot;&gt;&lt;span class=&quot;l-string&quot;&gt;                  &lt;span class=&quot;l-function-call&quot;&gt;result&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;+=&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;&amp;#39;[^/]*&amp;#39;&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;46&quot;&gt;&lt;span class=&quot;l-string&quot;&gt;                &lt;span class=&quot;l-function-call&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;else&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-function-call&quot;&gt;c&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;===&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;&amp;#39;?&amp;#39;&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;47&quot;&gt;&lt;span class=&quot;l-string&quot;&gt;                  &lt;span class=&quot;l-function-call&quot;&gt;result&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;+=&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;&amp;#39;[^/]&amp;#39;&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;48&quot;&gt;&lt;span class=&quot;l-string&quot;&gt;                &lt;span class=&quot;l-function-call&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;else&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;l-punctuation-bracket&quot;&gt;([&lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;&amp;#39;.&amp;#39;,&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;&amp;#39;+&amp;#39;&lt;/span&gt;&lt;span class=&quot;l-variable-parameter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;&amp;#39;^&amp;#39;&lt;/span&gt;&lt;span class=&quot;l-variable-parameter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;&amp;#39;$&amp;#39;&lt;/span&gt;&lt;span class=&quot;l-variable-parameter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;&amp;#39;{&amp;#39;&lt;/span&gt;&lt;span class=&quot;l-variable-parameter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;&amp;#39;}&amp;#39;&lt;/span&gt;&lt;span class=&quot;l-variable-parameter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;&amp;#39;(&amp;#39;&lt;/span&gt;&lt;span class=&quot;l-variable-parameter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;&amp;#39;)&amp;#39;&lt;/span&gt;&lt;span class=&quot;l-variable-parameter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;&amp;#39;|&amp;#39;&lt;/span&gt;&lt;span class=&quot;l-variable-parameter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;&amp;#39;[&amp;#39;&lt;/span&gt;&lt;span class=&quot;l-variable-parameter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;&amp;#39;]&amp;#39;&lt;/span&gt;&lt;span class=&quot;l-variable-parameter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;&amp;#39;\\&amp;#39;&lt;/span&gt;&lt;span class=&quot;l-variable-parameter&quot;&gt;].includes&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-variable-parameter&quot;&gt;c&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;))&lt;/span&gt; &lt;span class=&quot;l-punctuation-bracket&quot;&gt;{&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;49&quot;&gt;&lt;span class=&quot;l-string&quot;&gt;                  &lt;span class=&quot;l-function-call&quot;&gt;result&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;+=&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;&amp;#39;\\&amp;#39;&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;c&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;50&quot;&gt;&lt;span class=&quot;l-string&quot;&gt;                &lt;span class=&quot;l-punctuation-bracket&quot;&gt;}&lt;/span&gt; else &lt;span class=&quot;l-punctuation-bracket&quot;&gt;{&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;51&quot;&gt;&lt;span class=&quot;l-string&quot;&gt;                  &lt;span class=&quot;l-function-call&quot;&gt;result&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;+=&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;c&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;52&quot;&gt;&lt;span class=&quot;l-string&quot;&gt;                &lt;span class=&quot;l-punctuation-bracket&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;53&quot;&gt;&lt;span class=&quot;l-string&quot;&gt;              &lt;span class=&quot;l-function-call&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;54&quot;&gt;&lt;span class=&quot;l-string&quot;&gt;              return new RegExp&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;&amp;#39;^&amp;#39;&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;result&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;&amp;#39;$&amp;#39;&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;55&quot;&gt;&lt;span class=&quot;l-string&quot;&gt;            &lt;span class=&quot;l-punctuation-bracket&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;56&quot;&gt;&lt;span class=&quot;l-string&quot;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;57&quot;&gt;&lt;span class=&quot;l-string&quot;&gt;            &lt;span class=&quot;l-keyword-function&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;l-function&quot;&gt;isIgnored&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-function-call&quot;&gt;filePath,&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;patterns&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;l-punctuation-bracket&quot;&gt;{&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;58&quot;&gt;&lt;span class=&quot;l-string&quot;&gt;              &lt;span class=&quot;l-function-builtin&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;patterns.some&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-variable-parameter&quot;&gt;pattern&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;l-string-special-path&quot;&gt;{&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;59&quot;&gt;&lt;span class=&quot;l-string&quot;&gt;                &lt;span class=&quot;l-function-call&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;regex&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;globToRegex&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-function-call&quot;&gt;pattern&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;60&quot;&gt;&lt;span class=&quot;l-string&quot;&gt;                &lt;span class=&quot;l-function-call&quot;&gt;//&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;Patterns&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;without&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;a&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;slash&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;match&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;against&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;the&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;basename&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;well&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;61&quot;&gt;&lt;span class=&quot;l-string&quot;&gt;                &lt;span class=&quot;l-keyword-conditional&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-function-call&quot;&gt;!pattern.includes&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;&amp;#39;/&amp;#39;&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class=&quot;l-function-call&quot;&gt;!pattern.startsWith&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;&amp;#39;**/&amp;#39;&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;))&lt;/span&gt; &lt;span class=&quot;l-punctuation-bracket&quot;&gt;{&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;62&quot;&gt;&lt;span class=&quot;l-string&quot;&gt;                  &lt;span class=&quot;l-function-builtin&quot;&gt;return&lt;/span&gt; regex.test&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-function-call&quot;&gt;filePath&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;||&lt;/span&gt; &lt;span class=&quot;l-function-call&quot;&gt;regex.test&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-function-call&quot;&gt;path.basename&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-function-call&quot;&gt;filePath&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;))&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;63&quot;&gt;&lt;span class=&quot;l-string&quot;&gt;                &lt;span class=&quot;l-punctuation-bracket&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;64&quot;&gt;&lt;span class=&quot;l-string&quot;&gt;                &lt;span class=&quot;l-function-builtin&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;regex.test&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-function-call&quot;&gt;filePath&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;65&quot;&gt;&lt;span class=&quot;l-string&quot;&gt;              &lt;span class=&quot;l-function-call&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;66&quot;&gt;&lt;span class=&quot;l-string&quot;&gt;            &lt;span class=&quot;l-function-call&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;67&quot;&gt;&lt;span class=&quot;l-string&quot;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;68&quot;&gt;&lt;span class=&quot;l-string&quot;&gt;            &lt;span class=&quot;l-function-call&quot;&gt;//&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;Fetch&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;all&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;changed&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;files&lt;/span&gt; &lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;paginated, GitHub caps at 3 000)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;69&quot;&gt;&lt;span class=&quot;l-string&quot;&gt;            const allFiles = [];&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;70&quot;&gt;&lt;span class=&quot;l-string&quot;&gt;            for (let page = 1; ; page++) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;71&quot;&gt;&lt;span class=&quot;l-string&quot;&gt;              const { data: batch &lt;span class=&quot;l-punctuation-bracket&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;github.rest.pulls.listFiles&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;({&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;72&quot;&gt;&lt;span class=&quot;l-string&quot;&gt;                &lt;span class=&quot;l-function-call&quot;&gt;owner:&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;context.repo.owner,&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;73&quot;&gt;&lt;span class=&quot;l-string&quot;&gt;                &lt;span class=&quot;l-function-call&quot;&gt;repo:&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;context.repo.repo,&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;74&quot;&gt;&lt;span class=&quot;l-string&quot;&gt;                &lt;span class=&quot;l-function-call&quot;&gt;pull_number:&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;context.issue.number,&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;75&quot;&gt;&lt;span class=&quot;l-string&quot;&gt;                &lt;span class=&quot;l-function-call&quot;&gt;per_page:&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;100,&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;76&quot;&gt;&lt;span class=&quot;l-string&quot;&gt;                &lt;span class=&quot;l-function-call&quot;&gt;page,&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;77&quot;&gt;&lt;span class=&quot;l-string&quot;&gt;              &lt;span class=&quot;l-punctuation-bracket&quot;&gt;})&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;78&quot;&gt;&lt;span class=&quot;l-string&quot;&gt;              &lt;span class=&quot;l-function-call&quot;&gt;allFiles.push&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-function-call&quot;&gt;...batch&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;79&quot;&gt;&lt;span class=&quot;l-string&quot;&gt;              &lt;span class=&quot;l-keyword-conditional&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-function-call&quot;&gt;batch.length&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;l-number&quot;&gt;100&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt; break&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;80&quot;&gt;&lt;span class=&quot;l-string&quot;&gt;            &lt;span class=&quot;l-function-call&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;81&quot;&gt;&lt;span class=&quot;l-string&quot;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;82&quot;&gt;&lt;span class=&quot;l-string&quot;&gt;            &lt;span class=&quot;l-function-call&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;relevant&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;allFiles.filter&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;f =&amp;gt; !isIgnored(f.filename, ignorePatterns));&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;83&quot;&gt;&lt;span class=&quot;l-string&quot;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;84&quot;&gt;&lt;span class=&quot;l-string&quot;&gt;            function isTestFile(filePath) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;85&quot;&gt;&lt;span class=&quot;l-string&quot;&gt;              const base = path.basename(filePath);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;86&quot;&gt;&lt;span class=&quot;l-string&quot;&gt;              return (&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;87&quot;&gt;&lt;span class=&quot;l-string&quot;&gt;                /Test(s)?\.php$/.test(base) ||&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;88&quot;&gt;&lt;span class=&quot;l-string&quot;&gt;                /\.(spec|test)\.(ts|js|tsx|jsx)$/.test(base) ||&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;89&quot;&gt;&lt;span class=&quot;l-string&quot;&gt;                /^tests\//.test(filePath) ||&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;90&quot;&gt;&lt;span class=&quot;l-string&quot;&gt;                /\/tests\//.test(filePath) ||&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;91&quot;&gt;&lt;span class=&quot;l-string&quot;&gt;                /\/test\//.test(filePath)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;92&quot;&gt;&lt;span class=&quot;l-string&quot;&gt;              );&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;93&quot;&gt;&lt;span class=&quot;l-string&quot;&gt;            &lt;span class=&quot;l-punctuation-bracket&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;94&quot;&gt;&lt;span class=&quot;l-string&quot;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;95&quot;&gt;&lt;span class=&quot;l-string&quot;&gt;            // &lt;span class=&quot;l-variable-parameter&quot;&gt;Score&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;each&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;file&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;with&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;weighted&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;contributions:&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;96&quot;&gt;&lt;span class=&quot;l-string&quot;&gt;            &lt;span class=&quot;l-function-call&quot;&gt;//&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;Deleted&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;files:&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;lines&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;at&lt;/span&gt; &lt;span class=&quot;l-number&quot;&gt;20&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;%&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;weight,&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;file&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;count&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;at&lt;/span&gt; &lt;span class=&quot;l-number&quot;&gt;10&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;97&quot;&gt;&lt;span class=&quot;l-string&quot;&gt;            &lt;span class=&quot;l-function-call&quot;&gt;//&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;Test&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;files:&lt;/span&gt;    &lt;span class=&quot;l-variable-parameter&quot;&gt;additions&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;at&lt;/span&gt; &lt;span class=&quot;l-number&quot;&gt;50&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;%,&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;deletions&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;at&lt;/span&gt; &lt;span class=&quot;l-number&quot;&gt;15&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;%,&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;file&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;count&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;at&lt;/span&gt; &lt;span class=&quot;l-number&quot;&gt;10&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;98&quot;&gt;&lt;span class=&quot;l-string&quot;&gt;            &lt;span class=&quot;l-function-call&quot;&gt;//&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;Regular&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;files:&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;additions&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;at&lt;/span&gt; &lt;span class=&quot;l-number&quot;&gt;100&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;%,&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;deletions&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;at&lt;/span&gt; &lt;span class=&quot;l-number&quot;&gt;30&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;%,&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;file&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;count&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;at&lt;/span&gt; &lt;span class=&quot;l-number&quot;&gt;20&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;99&quot;&gt;&lt;span class=&quot;l-string&quot;&gt;            &lt;span class=&quot;l-function-builtin&quot;&gt;let&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;score&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;l-number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;100&quot;&gt;&lt;span class=&quot;l-string&quot;&gt;            &lt;span class=&quot;l-function-builtin&quot;&gt;let&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;additions&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;0,&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;deletions&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;l-number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;101&quot;&gt;&lt;span class=&quot;l-string&quot;&gt;            &lt;span class=&quot;l-keyword-repeat&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-function-call&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;f&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;of&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;relevant&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;102&quot;&gt;&lt;span class=&quot;l-string&quot;&gt;              &lt;span class=&quot;l-function-call&quot;&gt;additions&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;+=&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;f.additions&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;103&quot;&gt;&lt;span class=&quot;l-string&quot;&gt;              &lt;span class=&quot;l-function-call&quot;&gt;deletions&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;+=&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;f.deletions&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;104&quot;&gt;&lt;span class=&quot;l-string&quot;&gt;              &lt;span class=&quot;l-keyword-conditional&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-function-call&quot;&gt;f.status&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;===&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;&amp;#39;removed&amp;#39;&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;l-punctuation-bracket&quot;&gt;{&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;105&quot;&gt;&lt;span class=&quot;l-string&quot;&gt;                &lt;span class=&quot;l-function-call&quot;&gt;score&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;+=&lt;/span&gt; &lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;f.additions + f.deletions) * 0.2 + 10;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;106&quot;&gt;&lt;span class=&quot;l-string&quot;&gt;              &lt;span class=&quot;l-punctuation-bracket&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;l-keyword-conditional&quot;&gt;else&lt;/span&gt; &lt;span class=&quot;l-keyword-conditional&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-function-call&quot;&gt;isTestFile&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-function-call&quot;&gt;f.filename&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;))&lt;/span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;107&quot;&gt;&lt;span class=&quot;l-string&quot;&gt;                &lt;span class=&quot;l-function-call&quot;&gt;score&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;+=&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;f.additions&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;0.5&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;f.deletions&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;0.15&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;l-number&quot;&gt;10&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;108&quot;&gt;&lt;span class=&quot;l-string&quot;&gt;              &lt;span class=&quot;l-function-call&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;else&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;{&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;109&quot;&gt;&lt;span class=&quot;l-string&quot;&gt;                &lt;span class=&quot;l-function-call&quot;&gt;score&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;+=&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;f.additions&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;f.deletions&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;0.3&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;l-number&quot;&gt;20&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;110&quot;&gt;&lt;span class=&quot;l-string&quot;&gt;              &lt;span class=&quot;l-function-call&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;111&quot;&gt;&lt;span class=&quot;l-string&quot;&gt;            &lt;span class=&quot;l-function-call&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;112&quot;&gt;&lt;span class=&quot;l-string&quot;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;113&quot;&gt;&lt;span class=&quot;l-string&quot;&gt;            &lt;span class=&quot;l-function-call&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;filesChanged&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;relevant.length&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;114&quot;&gt;&lt;span class=&quot;l-string&quot;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;115&quot;&gt;&lt;span class=&quot;l-string&quot;&gt;            &lt;span class=&quot;l-function-call&quot;&gt;core.info&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;`&lt;span class=&quot;l-function-call&quot;&gt;Relevant&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;files:&lt;/span&gt; &lt;span class=&quot;l-punctuation-special&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;l-variable&quot;&gt;filesChanged&lt;/span&gt;&lt;span class=&quot;l-punctuation-special&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;+&lt;/span&gt;&lt;span class=&quot;l-punctuation-special&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;l-variable&quot;&gt;additions&lt;/span&gt;&lt;span class=&quot;l-punctuation-special&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;l-punctuation-special&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;l-variable&quot;&gt;deletions&lt;/span&gt;&lt;span class=&quot;l-punctuation-special&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;l-function-call&quot;&gt;score:&lt;/span&gt; ${&lt;span class=&quot;l-variable&quot;&gt;score&lt;/span&gt;.toFixed(1)&lt;span class=&quot;l-punctuation-bracket&quot;&gt;}&lt;/span&gt;`&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;116&quot;&gt;&lt;span class=&quot;l-string&quot;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;117&quot;&gt;&lt;span class=&quot;l-string&quot;&gt;            &lt;span class=&quot;l-function-call&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;sizeLabel&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;score&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;l-string-special-path&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;l-number&quot;&gt;250&lt;/span&gt; &lt;span class=&quot;l-string-special-path&quot;&gt;?&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;&amp;#39;small&amp;#39;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;118&quot;&gt;&lt;span class=&quot;l-string&quot;&gt;                            &lt;span class=&quot;l-function-builtin&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;score&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;l-string-special-path&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;l-number&quot;&gt;900&lt;/span&gt; &lt;span class=&quot;l-string-special-path&quot;&gt;?&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;&amp;#39;medium&amp;#39;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;119&quot;&gt;&lt;span class=&quot;l-string&quot;&gt;                            &lt;span class=&quot;l-function-builtin&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;score&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;l-string-special-path&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;l-number&quot;&gt;1800&lt;/span&gt; &lt;span class=&quot;l-string-special-path&quot;&gt;?&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;&amp;#39;large&amp;#39;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;120&quot;&gt;&lt;span class=&quot;l-string&quot;&gt;                            &lt;span class=&quot;l-function-builtin&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;&amp;#39;not-reviewable&amp;#39;&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;121&quot;&gt;&lt;span class=&quot;l-string&quot;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;122&quot;&gt;&lt;span class=&quot;l-string&quot;&gt;            &lt;span class=&quot;l-function-call&quot;&gt;//&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;Swap&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;out&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;any&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;existing&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;size&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;label&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;123&quot;&gt;&lt;span class=&quot;l-string&quot;&gt;            &lt;span class=&quot;l-function-call&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;SIZE_LABELS&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;&amp;#39;small&amp;#39;&lt;/span&gt;&lt;span class=&quot;l-variable-parameter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;&amp;#39;medium&amp;#39;&lt;/span&gt;&lt;span class=&quot;l-variable-parameter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;&amp;#39;large&amp;#39;&lt;/span&gt;&lt;span class=&quot;l-variable-parameter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;&amp;#39;not-reviewable&amp;#39;&lt;/span&gt;&lt;span class=&quot;l-variable-parameter&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;124&quot;&gt;&lt;span class=&quot;l-string&quot;&gt;            &lt;span class=&quot;l-function-call&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;data:&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;currentLabels&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;github.rest.issues.listLabelsOnIssue&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;({&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;125&quot;&gt;&lt;span class=&quot;l-string&quot;&gt;              &lt;span class=&quot;l-function-call&quot;&gt;owner:&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;context.repo.owner,&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;126&quot;&gt;&lt;span class=&quot;l-string&quot;&gt;              &lt;span class=&quot;l-function-call&quot;&gt;repo:&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;context.repo.repo,&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;127&quot;&gt;&lt;span class=&quot;l-string&quot;&gt;              &lt;span class=&quot;l-function-call&quot;&gt;issue_number:&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;context.issue.number,&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;128&quot;&gt;&lt;span class=&quot;l-string&quot;&gt;            &lt;span class=&quot;l-punctuation-bracket&quot;&gt;})&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;129&quot;&gt;&lt;span class=&quot;l-string&quot;&gt;            &lt;span class=&quot;l-keyword-repeat&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;const label of currentLabels) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;130&quot;&gt;&lt;span class=&quot;l-string&quot;&gt;              if (SIZE_LABELS.includes(label.name) &amp;amp;&amp;amp; label.name !== sizeLabel) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;131&quot;&gt;&lt;span class=&quot;l-string&quot;&gt;                await github.rest.issues.removeLabel({&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;132&quot;&gt;&lt;span class=&quot;l-string&quot;&gt;                  owner: context.repo.owner,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;133&quot;&gt;&lt;span class=&quot;l-string&quot;&gt;                  repo: context.repo.repo,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;134&quot;&gt;&lt;span class=&quot;l-string&quot;&gt;                  issue_number: context.issue.number,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;135&quot;&gt;&lt;span class=&quot;l-string&quot;&gt;                  name: label.name,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;136&quot;&gt;&lt;span class=&quot;l-string&quot;&gt;                &lt;span class=&quot;l-punctuation-bracket&quot;&gt;}&lt;/span&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;137&quot;&gt;&lt;span class=&quot;l-string&quot;&gt;              &lt;span class=&quot;l-punctuation-bracket&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;138&quot;&gt;&lt;span class=&quot;l-string&quot;&gt;            &lt;span class=&quot;l-punctuation-bracket&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;139&quot;&gt;&lt;span class=&quot;l-string&quot;&gt;            &lt;span class=&quot;l-keyword-conditional&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-function-call&quot;&gt;!currentLabels.some&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-function-call&quot;&gt;l&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;l-string-special-path&quot;&gt;l.name&lt;/span&gt; &lt;span class=&quot;l-string-special-path&quot;&gt;===&lt;/span&gt; &lt;span class=&quot;l-string-special-path&quot;&gt;sizeLabel&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;))&lt;/span&gt; &lt;span class=&quot;l-punctuation-bracket&quot;&gt;{&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;140&quot;&gt;&lt;span class=&quot;l-string&quot;&gt;              &lt;span class=&quot;l-function-call&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;github.rest.issues.addLabels&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;({&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;141&quot;&gt;&lt;span class=&quot;l-string&quot;&gt;                &lt;span class=&quot;l-function-call&quot;&gt;owner:&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;context.repo.owner,&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;142&quot;&gt;&lt;span class=&quot;l-string&quot;&gt;                &lt;span class=&quot;l-function-call&quot;&gt;repo:&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;context.repo.repo,&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;143&quot;&gt;&lt;span class=&quot;l-string&quot;&gt;                &lt;span class=&quot;l-function-call&quot;&gt;issue_number:&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;context.issue.number,&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;144&quot;&gt;&lt;span class=&quot;l-string&quot;&gt;                &lt;span class=&quot;l-function-call&quot;&gt;labels:&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;[sizeLabel],&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;145&quot;&gt;&lt;span class=&quot;l-string&quot;&gt;              &lt;span class=&quot;l-punctuation-bracket&quot;&gt;})&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;146&quot;&gt;&lt;span class=&quot;l-string&quot;&gt;            &lt;span class=&quot;l-punctuation-bracket&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;147&quot;&gt;&lt;span class=&quot;l-string&quot;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;148&quot;&gt;&lt;span class=&quot;l-string&quot;&gt;            &lt;span class=&quot;l-function-call&quot;&gt;core.info&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;`&lt;span class=&quot;l-function-call&quot;&gt;Applied&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;label:&lt;/span&gt; &lt;span class=&quot;l-punctuation-special&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;l-variable&quot;&gt;sizeLabel&lt;/span&gt;&lt;span class=&quot;l-punctuation-special&quot;&gt;}&lt;/span&gt;`&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;Things to keep in mind&lt;/h1&gt;
&lt;p&gt;&lt;strong&gt;Create the &lt;code&gt;not-reviewable&lt;/code&gt; label first.&lt;/strong&gt; GitHub's label API throws if you try to apply a label that doesn't exist in the repository. Add it once through the UI or with the API before the workflow runs.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Test file detection is heuristic.&lt;/strong&gt; The patterns we use match our conventions. If your project puts tests in a &lt;code&gt;__tests__&lt;/code&gt; directory or uses a &lt;code&gt;.test.rb&lt;/code&gt; suffix, you'll need to adjust the &lt;code&gt;isTestFile&lt;/code&gt; function.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The thresholds are starting points.&lt;/strong&gt; What counts as &lt;code&gt;small&lt;/code&gt; depends entirely on your team's pace and context. We tuned ours over a few weeks by looking at which PRs felt easy and which felt painful, and adjusting until the labels matched our intuition. Start with something reasonable and iterate.&lt;/p&gt;
&lt;p&gt;PR size labels are a low-effort, high-signal addition to any review workflow. The key insight behind the weighted model is that &lt;em&gt;not all lines are equal&lt;/em&gt;. A deleted file, a test assertion, and a new service class represent very different amounts of reviewer work. Treating them the same produces labels that mislead; treating them differently produces labels that actually help.&lt;/p&gt;
&lt;p&gt;The workflow is about 100 lines of JavaScript inside a YAML file. It's easy to read, easy to adapt, and it has meaningfully changed how we queue and prioritise reviews at Contractify.&lt;/p&gt;&lt;p&gt;&lt;a href=&quot;https://www.yellowduck.be/tags/javascript&quot;&gt;#javascript&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/development&quot;&gt;#development&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/best-practice&quot;&gt;#best-practice&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/github&quot;&gt;#github&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/git&quot;&gt;#git&lt;/a&gt;&lt;/p&gt;</content>
  </entry>
  <entry>
    <id>https://www.yellowduck.be/posts/debugging-a-502-on-a-signed-redirect-nginxs-upstream-sent-too-big-header</id>
    <title>🐥 Debugging a 502 on a Signed Redirect: nginx's &quot;upstream sent too big header&quot;</title>
    <updated>2026-10-26T18:00:00Z</updated>
    <published>2026-10-26T18:00:00Z</published>
    <author><name>Pieter Claerhout</name><email>pieter@yellowduck.be</email></author>
    <link rel="alternate" href="https://www.yellowduck.be/posts/debugging-a-502-on-a-signed-redirect-nginxs-upstream-sent-too-big-header"/>
    <content type="html">&lt;p&gt;You've built a signed redirect endpoint. It validates a token, generates a URL, and sends the browser on its way with a &lt;code&gt;302&lt;/code&gt;. It works perfectly in local development but fails with a cryptic &lt;strong&gt;502 Bad Gateway&lt;/strong&gt; in staging. Here's how to diagnose and fix it without touching your web server config.&lt;/p&gt;
&lt;h1&gt;The symptom&lt;/h1&gt;
&lt;p&gt;A &lt;code&gt;curl -v&lt;/code&gt; request to the endpoint returns nginx's own 502 page—not a Laravel or application error page, just nginx's default 150-byte HTML. That detail matters: it means PHP-FPM never delivered a response nginx could forward. The application may have produced &lt;em&gt;something&lt;/em&gt;, but nginx rejected it before the browser ever saw it.&lt;/p&gt;
&lt;h1&gt;Finding the root cause&lt;/h1&gt;
&lt;p&gt;The nginx error log is the first place to look:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-plaintext&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;span class=&quot;l-line&quot; data-line=&quot;1&quot;&gt;upstream sent too big header while reading response header from upstream&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That single line explains everything. nginx has a default buffer for reading response headers from an upstream FastCGI process (&lt;code&gt;fastcgi_buffer_size&lt;/code&gt;, which defaults to 4 KB). The &lt;code&gt;302&lt;/code&gt; response your application returned was otherwise valid, but its &lt;code&gt;Location&lt;/code&gt; header was too long to fit in that buffer. nginx dropped the whole response and returned a 502.&lt;/p&gt;
&lt;p&gt;This happens frequently with e-signature providers, OAuth flows, and any service that encodes its entire state (session, return URL, signature, expiry) in a redirect URL. Those URLs routinely run to several kilobytes.&lt;/p&gt;
&lt;h1&gt;Why it only fails in staging&lt;/h1&gt;
&lt;p&gt;Local development typically uses the built-in PHP server or a framework dev server, neither of which imposes nginx's header buffer limits. The problem is invisible until nginx sits between the application and the browser—which is the case in every hosted environment but rarely in local development.&lt;/p&gt;
&lt;h1&gt;The obvious fix and why to think twice&lt;/h1&gt;
&lt;p&gt;The textbook answer is to raise nginx's buffer size:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-nginx&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;span class=&quot;l-line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;l-keyword&quot;&gt;fastcgi_buffer_size&lt;/span&gt; &lt;span class=&quot;l-number&quot;&gt;32k&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;2&quot;&gt;&lt;span class=&quot;l-keyword&quot;&gt;fastcgi_buffers&lt;/span&gt; &lt;span class=&quot;l-number&quot;&gt;16&lt;/span&gt; &lt;span class=&quot;l-number&quot;&gt;32k&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This works, but it requires access to the server config and a reload. It also papers over the underlying issue: you're transmitting a very large URL in an HTTP header, which is inherently fragile. Different proxies, CDNs, and load balancers have their own header size limits, and you may hit another one later.&lt;/p&gt;
&lt;h1&gt;A better fix: move the URL out of the header&lt;/h1&gt;
&lt;p&gt;HTTP redirects put the destination in a &lt;code&gt;Location&lt;/code&gt; &lt;em&gt;header&lt;/em&gt;. If instead you put the URL in the response &lt;em&gt;body&lt;/em&gt;, header buffer limits are irrelevant—response bodies are streamed, not buffered in header-size chunks.&lt;/p&gt;
&lt;p&gt;The simplest implementation is a minimal HTML page with a &lt;code&gt;&amp;lt;meta&amp;gt;&lt;/code&gt; refresh and a JavaScript fallback:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-php&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;span class=&quot;l-line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;l-line&quot; data-line=&quot;2&quot;&gt;&lt;span class=&quot;l-variable&quot;&gt;$url&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;l-variable-builtin&quot;&gt;$this&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;l-function-method-call&quot;&gt;buildSigningUrl&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-variable&quot;&gt;$signer&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;3&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;4&quot;&gt;&lt;span class=&quot;l-keyword-return&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;l-function-call&quot;&gt;response&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;5&quot;&gt;    &lt;span class=&quot;l-string&quot;&gt;&amp;#39;&amp;lt;html&amp;gt;&amp;lt;head&amp;gt;&amp;lt;meta http-equiv=&amp;quot;refresh&amp;quot; content=&amp;quot;0;url=&amp;#39;&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;.&lt;/span&gt; &lt;span class=&quot;l-function-call&quot;&gt;e&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-variable&quot;&gt;$url&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;.&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;&amp;#39;&amp;quot;&amp;gt;&amp;lt;/head&amp;gt;&amp;#39;&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;6&quot;&gt;    &lt;span class=&quot;l-string&quot;&gt;&amp;#39;&amp;lt;body&amp;gt;&amp;lt;script&amp;gt;window.location.replace(&amp;#39;&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;.&lt;/span&gt; &lt;span class=&quot;l-function-call&quot;&gt;json_encode&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-variable&quot;&gt;$url&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;.&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;&amp;#39;)&amp;lt;/script&amp;gt;&amp;lt;/body&amp;gt;&amp;lt;/html&amp;gt;&amp;#39;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;7&quot;&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The browser executes the redirect client-side in under a frame. From the user's perspective, it's indistinguishable from a &lt;code&gt;302&lt;/code&gt;. From nginx's perspective, it's a normal 200 response with a small body—no oversized headers and no 502.&lt;/p&gt;
&lt;h2&gt;Why not use a URL shortener or cache indirection?&lt;/h2&gt;
&lt;p&gt;An alternative is to store the long URL behind a short token and redirect the browser to a local endpoint that looks it up:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-plaintext&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;span class=&quot;l-line&quot; data-line=&quot;1&quot;&gt;/esign/{uuid}?redirect=1  →  /esign/forward/{token}  →  {long signing URL}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;em&gt;first&lt;/em&gt; redirect is short and fits in nginx's buffer. But the &lt;em&gt;second&lt;/em&gt; redirect—from your forward endpoint to the signing URL—still goes through PHP-FPM and nginx, still puts the long URL in a &lt;code&gt;Location&lt;/code&gt; header, and still hits the same limit. You've added a round trip without solving anything.&lt;/p&gt;
&lt;p&gt;The only indirection that works is one where the browser makes the final hop to the long URL, not your server—which is exactly what the HTML body approach does.&lt;/p&gt;
&lt;h1&gt;The diagnostic checklist&lt;/h1&gt;
&lt;p&gt;If you hit a 502 on a redirect endpoint and can't reproduce it locally:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;curl -v&lt;/code&gt; the URL—check whether the response is an application error page or nginx's own 502 page. Nginx's own page means the problem is between nginx and upstream, not inside your application.&lt;/li&gt;
&lt;li&gt;Check &lt;code&gt;/var/log/nginx/error.log&lt;/code&gt; around the timestamp. &lt;code&gt;upstream sent too big header&lt;/code&gt; points to buffer limits; &lt;code&gt;connect() failed&lt;/code&gt; or &lt;code&gt;upstream timed out&lt;/code&gt; point elsewhere.&lt;/li&gt;
&lt;li&gt;Check PHP-FPM's log for crashes or OOM kills if the nginx log is silent.&lt;/li&gt;
&lt;li&gt;Measure the URL you're redirecting to. If it's over 3–4 KB, the buffer is likely the culprit.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The pattern generalizes beyond PHP. Any application server behind nginx—Python/WSGI, Node, or Go—can trigger the same error with a large redirect. The fix is the same: put the long URL in the body, not a header.&lt;/p&gt;&lt;p&gt;&lt;a href=&quot;https://www.yellowduck.be/tags/laravel&quot;&gt;#laravel&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/php&quot;&gt;#php&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/http&quot;&gt;#http&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/devops&quot;&gt;#devops&lt;/a&gt;&lt;/p&gt;</content>
  </entry>
  <entry>
    <id>https://www.yellowduck.be/posts/why-phps-recent-critical-rces-are-an-architectural-non-problem-in-elixir</id>
    <title>🐥 Why PHP's Recent Critical RCEs Are an Architectural Non-Problem in Elixir</title>
    <updated>2026-10-25T18:00:00Z</updated>
    <published>2026-10-25T18:00:00Z</published>
    <author><name>Pieter Claerhout</name><email>pieter@yellowduck.be</email></author>
    <link rel="alternate" href="https://www.yellowduck.be/posts/why-phps-recent-critical-rces-are-an-architectural-non-problem-in-elixir"/>
    <content type="html">&lt;p&gt;The cybersecurity landscape was shaken in October 2026 by the disclosure of two critical, unauthenticated Remote Code Execution (RCE) vulnerabilities in PHP affecting versions 8.2 through 8.5. Rated at a severe &lt;a href=&quot;https://www.first.org/cvss/v4-0/&quot;&gt;CVSS v4.0&lt;/a&gt; score of 9.2, these vulnerabilities allow attackers to compromise servers before any application-level security code even executes.&lt;/p&gt;
&lt;p&gt;Whenever major security events occur in mainstream runtime environments, it is worth examining how alternative language ecosystems handle the same vectors. For teams using Elixir and the BEAM virtual machine, these specific vulnerabilities are not just avoided by careful coding—they are entirely neutralized by the runtime's underlying architecture.&lt;/p&gt;
&lt;p&gt;Here is a deep technical breakdown of why the structural flaws exposed in PHP's 2026 advisories are a non-problem in Elixir, followed by how to safely manage memory and package security in the BEAM ecosystem.&lt;/p&gt;
&lt;h1&gt;Vulnerability 1: The OOM Partial Initialization Attack (CVE-2026-109618)&lt;/h1&gt;
&lt;h2&gt;What Happened in PHP&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://nvd.nist.gov/vuln/detail/CVE-2026-109618&quot;&gt;CVE-2026-109618&lt;/a&gt; (tracked via &lt;a href=&quot;https://github.com/advisories/GHSA-w8hw-w9fx-j79w&quot;&gt;GHSA-w8hw-w9fx-j79w&lt;/a&gt;) describes an Out-of-Memory (OOM) corruption flaw. When a web request hits a PHP worker, the runtime allocates memory to initialize internal request data structures. If an allocation failure occurs mid-initialization, the process is interrupted, leaving the request data in a partially formed state.&lt;/p&gt;
&lt;p&gt;During the subsequent request cleanup phase, PHP's memory management engine misinterprets this malformed, incomplete internal data as valid data structure pointers. Attackers can weaponize this behavior to hijack the pointer execution path, achieving unauthenticated Remote Code Execution.&lt;/p&gt;
&lt;h2&gt;Why It Is a Non-Problem in Elixir&lt;/h2&gt;
&lt;p&gt;The BEAM virtual machine manages memory in a completely decentralized fashion compared to traditional monolithic runtimes.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Per-Process Heaps:&lt;/strong&gt; In Elixir, every concurrent operation (like an incoming HTTP request handled by &lt;a href=&quot;https://github.com/ninenines/cowboy&quot;&gt;Cowboy&lt;/a&gt; or &lt;a href=&quot;https://github.com/mtrudel/bandit&quot;&gt;Bandit&lt;/a&gt; in a &lt;a href=&quot;https://phoenixframework.org&quot;&gt;Phoenix&lt;/a&gt; application) runs inside its own isolated, lightweight BEAM process. Each process contains its own private heap. There is no massive, shared global heap for request initialization.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Fail-Stop Mentality (&amp;quot;Let it Crash&amp;quot;):&lt;/strong&gt; If an Elixir process encounters a fatal error—such as allocating memory beyond a configured limit—the runtime does not attempt a delicate, complex cleanup dance over partially initialized, untyped memory structures. The BEAM instantly kills the isolated process.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Immutability and Type Safety:&lt;/strong&gt; Because data structures in Elixir are immutable and managed strictly by a type-safe virtual machine layer, a crash during process creation simply causes the supervision tree to discard the process ID (PID) and reclaim the isolated heap in one sweeping, atomic operation. There are no dangling initialization pointers to misinterpret.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Defensive Engineering: Monitoring BEAM Process Memory Thresholds&lt;/h2&gt;
&lt;p&gt;While an OOM error cannot corrupt the BEAM runtime state, a runaway process consuming too much memory can still starve the operating system. Elixir allows you to proactively enforce memory limits at the individual process level to safely drop abnormal requests before they impact the node.&lt;/p&gt;
&lt;p&gt;You can set a max memory threshold dynamically for temporary request processes using &lt;a href=&quot;https://hexdocs.pm/elixir/Process.html#flag/2&quot;&gt;&lt;code&gt;Process.flag/2&lt;/code&gt;&lt;/a&gt;:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-elixir&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;span class=&quot;l-line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;l-comment&quot;&gt;# Enforcing a 50MB memory ceiling on a specific task process&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;2&quot;&gt;&lt;span class=&quot;l-module&quot;&gt;Task.Supervisor&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;l-function-call&quot;&gt;start_child&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-module&quot;&gt;MyTaskSupervisor&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-keyword&quot;&gt;fn&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;3&quot;&gt;  &lt;span class=&quot;l-module&quot;&gt;Process&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;l-function-call&quot;&gt;flag&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-string-special-symbol&quot;&gt;:max_heap_size&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-punctuation-special&quot;&gt;%&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;l-string-special-symbol&quot;&gt;size: &lt;/span&gt;&lt;span class=&quot;l-number&quot;&gt;6_250_000&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-string-special-symbol&quot;&gt;kill: &lt;/span&gt;&lt;span class=&quot;l-boolean&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-string-special-symbol&quot;&gt;error_logger: &lt;/span&gt;&lt;span class=&quot;l-boolean&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;})&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;4&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;5&quot;&gt;  &lt;span class=&quot;l-comment&quot;&gt;# Process payload here...&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;6&quot;&gt;  &lt;span class=&quot;l-function-call&quot;&gt;perform_heavy_lifting&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;()&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;7&quot;&gt;&lt;span class=&quot;l-keyword&quot;&gt;end&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;size&lt;/code&gt;:&lt;/strong&gt; Measured in machine words (1 word = 8 bytes on 64-bit systems). &lt;code&gt;6_250_000&lt;/code&gt; words equals exactly 50 MB.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;kill: true&lt;/code&gt;:&lt;/strong&gt; Instructs the BEAM to instantly kill the process if it crosses the limit, rather than letting it slow down the node with emergency garbage collection loops.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;error_logger: true&lt;/code&gt;:&lt;/strong&gt; Automatically logs a descriptive crash dump so security and operations teams can audit potential memory-exhaustion attack vectors.&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;Vulnerability 2: Stale Worker Global State (CVE-2026-109619)&lt;/h1&gt;
&lt;h2&gt;What Happened in PHP&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://nvd.nist.gov/vuln/detail/CVE-2026-109619&quot;&gt;CVE-2026-109619&lt;/a&gt; (tracked via &lt;a href=&quot;https://github.com/advisories/GHSA-6wmf-4rv5-p3rp&quot;&gt;GHSA-6wmf-4rv5-p3rp&lt;/a&gt;) is a Use-After-Free variant tied to persistent worker state. PHP setups typically rely on persistent processes (like &lt;a href=&quot;https://www.php.net/manual/en/install.fpm.php&quot;&gt;PHP-FPM&lt;/a&gt;, LiteSpeed, or Apache mod_php modules) to handle thousands of serial HTTP requests over their lifespan to avoid cold-start overhead.&lt;/p&gt;
&lt;p&gt;In this exploit, request-specific internal state managed to persist in the worker's memory space even after its backing allocation was freed. When a subsequent, unrelated HTTP request was assigned to that exact same worker thread, the worker referenced that stale, dangling memory state before normal initialization routines could clean it. This state bleeding creates a predictable memory window for arbitrary code execution. An informational advisory (&lt;a href=&quot;https://nvd.nist.gov/vuln/detail/CVE-2026-115560&quot;&gt;CVE-2026-115560&lt;/a&gt;) was later issued regarding session upload progress handlers interacting with this state.&lt;/p&gt;
&lt;h2&gt;Why It Is a Non-Problem in Elixir&lt;/h2&gt;
&lt;p&gt;This vulnerability represents a classic failure of the Shared-State Architecture. Elixir completely eradicates this entire attack vector using a Share-Nothing Architecture driven by the &lt;a href=&quot;https://en.wikipedia.org/wiki/Actor_model&quot;&gt;Actor Model&lt;/a&gt;.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Zero State Leakage Between Requests:&lt;/strong&gt; When a web request reaches an Elixir web server, a completely new BEAM process is spawned. When the response is sent, that process terminates. Because memory is not shared between processes, it is physically impossible for Request B to access, inherit, or accidentally execute memory allocated by Request A.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;No Mutability, No Side-Effects:&lt;/strong&gt; A major factor in the PHP exploit is that runtime configuration and state can be mutated dynamically at request runtime. In Elixir, data is entirely immutable. You cannot mutate an underlying global memory buffer during a request lifecycle because global mutable state does not exist in the traditional sense. Even stateful storage (like an &lt;a href=&quot;https://hexdocs.pm/elixir/Agent.html&quot;&gt;&lt;code&gt;Agent&lt;/code&gt;&lt;/a&gt; or &lt;a href=&quot;https://hexdocs.pm/elixir/GenServer.html&quot;&gt;&lt;code&gt;GenServer&lt;/code&gt;&lt;/a&gt;) relies on explicit message passing rather than shared pointer access.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h1&gt;Ecosystem Security: Managing Hex Third-Party Supply Chain Risks&lt;/h1&gt;
&lt;p&gt;While the BEAM protects you from core runtime vulnerabilities, any modern application is still susceptible to supply chain attacks via third-party libraries. PHP handles this via &lt;a href=&quot;https://getcomposer.org&quot;&gt;Composer&lt;/a&gt;; Elixir relies on the &lt;a href=&quot;https://hex.pm&quot;&gt;Hex&lt;/a&gt; package manager. Because the Elixir ecosystem is smaller, proactive supply chain auditing is critical.&lt;/p&gt;
&lt;p&gt;To secure your production pipeline against malicious dependency injections or unpatched packages, incorporate these three automated layers into your Continuous Integration (CI) suite:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;mix deps.unlock --check-unused&lt;/code&gt;:&lt;/strong&gt; Ensures that your &lt;code&gt;mix.lock&lt;/code&gt; file matches your configuration exactly and prevents dangling or untracked packages from sneaking into your builds.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;mix hex.audit&lt;/code&gt;:&lt;/strong&gt; Checks your local dependency tree against the Hex registry to find packages that have been retired by their maintainers due to security vulnerabilities or deprecation.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;https://github.com/nccgroup/sobelow&quot;&gt;Sobelow&lt;/a&gt; (Static Analysis):&lt;/strong&gt; Integrate Sobelow into your build process to scan Elixir code specifically for application-level vulnerability paths (like SQL injection or insecure deserialization in Phoenix applications).&lt;/li&gt;
&lt;/ol&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-bash&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;span class=&quot;l-line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;l-comment&quot;&gt;# Add to your CI workflow&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;2&quot;&gt;&lt;span class=&quot;l-function-call&quot;&gt;mix&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;deps.unlock&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;--check-unused&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;3&quot;&gt;&lt;span class=&quot;l-function-call&quot;&gt;mix&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;deps.get&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;4&quot;&gt;&lt;span class=&quot;l-function-call&quot;&gt;mix&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;hex.audit&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;5&quot;&gt;&lt;span class=&quot;l-function-call&quot;&gt;mix&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;sobelow&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;--config&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;.sobelow-conf&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;--exit&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;Architecture as a Security Shield&lt;/h1&gt;
&lt;p&gt;The October 2026 PHP Security Advisory demonstrates that even when you write flawless application code, you remain exposed to the memory mechanics of your underlying runtime platform. PHP engineers were forced to scramble to apply emergency configurations (such as setting &lt;code&gt;memory_limit=-1&lt;/code&gt; or disabling session upload progress features) as brittle stopgaps while waiting for patches.&lt;/p&gt;
&lt;p&gt;Elixir developers didn't have to change a single line of code or adjust their production topologies during this advisory cycle. By moving away from shared-memory, single-threaded worker abstractions and adopting the BEAM's immutable, process-isolated model, Elixir turns severe runtime engineering vulnerabilities into an architectural impossibility.&lt;/p&gt;
&lt;h1&gt;Architectural &amp;amp; Security Feature Summary&lt;/h1&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Security Risk / Feature&lt;/th&gt;
&lt;th&gt;PHP Runtime Environment (C-Based Engine)&lt;/th&gt;
&lt;th&gt;Elixir / BEAM Runtime Environment&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Primary CVE Focus&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://nvd.nist.gov/vuln/detail/CVE-2026-109618&quot;&gt;CVE-2026-109618&lt;/a&gt; &amp;amp; &lt;a href=&quot;https://nvd.nist.gov/vuln/detail/CVE-2026-109619&quot;&gt;CVE-2026-109619&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;None (architecturally immune to these vectors)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Concurrency Model&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Ephemeral request execution inside persistent, reusable worker processes.&lt;/td&gt;
&lt;td&gt;Spawns unique, highly isolated lightweight processes for every single request.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Memory Isolation&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Shared heap inside the worker process; vulnerable to pointer corruption.&lt;/td&gt;
&lt;td&gt;Share-nothing architecture; absolute memory boundaries per process.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Handling Allocation Faults&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Transitions into delicate cleanup paths that risk treating uninitialized state as valid.&lt;/td&gt;
&lt;td&gt;Immediate process termination (&amp;quot;Let it crash&amp;quot;) accompanied by atomic heap reclamation.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Process Control&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Handled globally via pool managers (e.g., &lt;a href=&quot;https://www.php.net/manual/en/install.fpm.configuration.php&quot;&gt;&lt;code&gt;php-fpm.conf&lt;/code&gt;&lt;/a&gt;).&lt;/td&gt;
&lt;td&gt;Granular, per-process heap caps using &lt;a href=&quot;https://hexdocs.pm/elixir/Process.html#flag/2&quot;&gt;&lt;code&gt;Process.flag(:max_heap_size, ...)&lt;/code&gt;&lt;/a&gt;.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Supply Chain Guardrails&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Mainstream security coverage via &lt;a href=&quot;https://getcomposer.org/doc/03-cli.md#audit&quot;&gt;Composer Audit&lt;/a&gt;.&lt;/td&gt;
&lt;td&gt;Targeted protection via &lt;code&gt;mix hex.audit&lt;/code&gt; and static application analysis with &lt;a href=&quot;https://github.com/nccgroup/sobelow&quot;&gt;Sobelow&lt;/a&gt;.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;p&gt;&lt;a href=&quot;https://www.yellowduck.be/tags/development&quot;&gt;#development&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/php&quot;&gt;#php&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/elixir&quot;&gt;#elixir&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/phoenix&quot;&gt;#phoenix&lt;/a&gt;&lt;/p&gt;</content>
  </entry>
  <entry>
    <id>https://www.yellowduck.be/posts/fixing-header-too-long-when-downloading-precompiled-nifs-in-elixir</id>
    <title>🐥 Fixing &quot;header_too_long&quot; When Downloading Precompiled NIFs in Elixir</title>
    <updated>2026-10-24T17:00:00Z</updated>
    <published>2026-10-24T17:00:00Z</published>
    <author><name>Pieter Claerhout</name><email>pieter@yellowduck.be</email></author>
    <link rel="alternate" href="https://www.yellowduck.be/posts/fixing-header-too-long-when-downloading-precompiled-nifs-in-elixir"/>
    <content type="html">&lt;p&gt;If you've recently run &lt;code&gt;mix compile&lt;/code&gt; on an Elixir project that uses precompiled NIFs, such as &lt;a href=&quot;https://github.com/leandrocp/mdex&quot;&gt;mdex&lt;/a&gt; or &lt;a href=&quot;https://github.com/leandrocp/lumis&quot;&gt;lumis&lt;/a&gt;, you may have seen an error like this:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-plaintext&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;span class=&quot;l-line&quot; data-line=&quot;1&quot;&gt;[info] Attempt 1 failed with {:error, &amp;quot;couldn&amp;#39;t fetch NIF from https://github.com/...: {:error, {:header_too_long, {:max, 10240}}}&amp;quot;}&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;2&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;3&quot;&gt;== Compilation error in file lib/mdex_native/native.ex ==&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;4&quot;&gt;** (RuntimeError) Error while downloading precompiled NIF: couldn&amp;#39;t fetch NIF from https://github.com/...&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is frustrating because &lt;code&gt;mix deps.get&lt;/code&gt; succeeds. The problem only appears during compilation.&lt;/p&gt;
&lt;h1&gt;What's Going On?&lt;/h1&gt;
&lt;p&gt;Elixir libraries that include native code (Rust, C, etc.) often ship precompiled binaries called NIFs (Native Implemented Functions), so you don't need a native toolchain to use them. The library &lt;a href=&quot;https://github.com/philss/rustler_precompiled&quot;&gt;&lt;code&gt;rustler_precompiled&lt;/code&gt;&lt;/a&gt; handles downloading the right binary for your platform at compile time.&lt;/p&gt;
&lt;p&gt;Under the hood, &lt;code&gt;rustler_precompiled&lt;/code&gt; uses &lt;a href=&quot;https://github.com/elixir-mint/mint&quot;&gt;Mint&lt;/a&gt;, an Elixir HTTP client, to fetch NIF archives from GitHub Releases.&lt;/p&gt;
&lt;p&gt;Here's the problem: GitHub's CDN now returns &lt;strong&gt;very large response headers&lt;/strong&gt; on release asset redirect requests—headers that exceed Mint's default maximum of &lt;strong&gt;10,240 bytes&lt;/strong&gt; (10 KB). When Mint hits that cap, it aborts the request with &lt;code&gt;{:header_too_long, {:max, 10240}}&lt;/code&gt;, and compilation fails.&lt;/p&gt;
&lt;p&gt;This isn't a bug in your code. It's an upstream issue involving GitHub's infrastructure and Mint's conservative defaults.&lt;/p&gt;
&lt;h1&gt;Three Ways to Fix It&lt;/h1&gt;
&lt;h2&gt;Option 1 — Download the NIF Manually with curl (Fastest)&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;curl&lt;/code&gt; doesn't have the same header size restriction. You can download the NIF archive directly and let &lt;code&gt;rustler_precompiled&lt;/code&gt; pick it up from its cache on the next compile.&lt;/p&gt;
&lt;p&gt;First, find the download URLs in the error message in your terminal, then fetch the archive:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-bash&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;span class=&quot;l-line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;l-function-call&quot;&gt;curl&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;-L&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;-o&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;/tmp/your_nif.tar.gz&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;&amp;quot;https://github.com/.../your_nif.tar.gz&amp;quot;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;rustler_precompiled&lt;/code&gt; caches NIFs in &lt;code&gt;~/.cache/rustler_precompiled/&lt;/code&gt; (Linux/macOS). Copy the downloaded archive there, then rerun &lt;code&gt;mix compile&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;Option 2 — Update the Dependency&lt;/h2&gt;
&lt;p&gt;The maintainers of these libraries are aware of the issue. A newer release may have switched away from Mint for the download step or increased Mint's header limit. Try:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-bash&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;span class=&quot;l-line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;l-function-call&quot;&gt;mix&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;deps.update&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;your_nif_package&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;2&quot;&gt;&lt;span class=&quot;l-function-call&quot;&gt;mix&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;compile&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is the cleanest long-term fix if a patched version is available.&lt;/p&gt;
&lt;h2&gt;Option 3 — Force Build from Source&lt;/h2&gt;
&lt;p&gt;If you have the &lt;a href=&quot;https://rustup.rs/&quot;&gt;Rust toolchain&lt;/a&gt; installed, you can tell &lt;code&gt;rustler_precompiled&lt;/code&gt; to compile the NIF locally instead of downloading it.&lt;/p&gt;
&lt;p&gt;Add this to &lt;code&gt;config/dev.exs&lt;/code&gt; (or &lt;code&gt;config/config.exs&lt;/code&gt;):&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-elixir&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;span class=&quot;l-line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;l-function-call&quot;&gt;config&lt;/span&gt; &lt;span class=&quot;l-string-special-symbol&quot;&gt;:rustler_precompiled&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-string-special-symbol&quot;&gt;:force_build&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-string-special-symbol&quot;&gt;your_nif_package: &lt;/span&gt;&lt;span class=&quot;l-boolean&quot;&gt;true&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And add Rustler as an optional dependency in &lt;code&gt;mix.exs&lt;/code&gt;:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-elixir&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;span class=&quot;l-line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;l-string-special-symbol&quot;&gt;:rustler&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;&amp;quot;&amp;gt;= 0.0.0&amp;quot;&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-string-special-symbol&quot;&gt;optional: &lt;/span&gt;&lt;span class=&quot;l-boolean&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then run &lt;code&gt;mix deps.get &amp;amp;&amp;amp; mix compile&lt;/code&gt;. Compilation will take longer because it builds native code, but it will work regardless of GitHub's header behavior.&lt;/p&gt;
&lt;h1&gt;Which Should You Choose?&lt;/h1&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Situation&lt;/th&gt;
&lt;th&gt;Recommended fix&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Just need to unblock right now&lt;/td&gt;
&lt;td&gt;Option 1 (manual curl)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A patched release is available&lt;/td&gt;
&lt;td&gt;Option 2 (update deps)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;You control the environment and have Rust&lt;/td&gt;
&lt;td&gt;Option 3 (force build)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CI/CD pipeline is broken&lt;/td&gt;
&lt;td&gt;Option 1 or 3, depending on whether Rust is available&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h1&gt;The Broader Lesson&lt;/h1&gt;
&lt;p&gt;This failure mode—a third-party infrastructure change silently breaking your build—is a good reminder to:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Pin your NIF dependency versions so you're not surprised by new behavior after an update.&lt;/li&gt;
&lt;li&gt;Cache precompiled NIFs in CI so you're not downloading them on every build.&lt;/li&gt;
&lt;li&gt;Have a fallback strategy (force build) for environments where downloading isn't reliable.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Precompiled NIFs are a major convenience for Elixir developers who don't want to manage native toolchains. But they introduce a network dependency at compile time, and as this issue shows, that dependency can break in unexpected ways.&lt;/p&gt;
&lt;h1&gt;The Proper Fix&lt;/h1&gt;
&lt;p&gt;The poeple behind &lt;a href=&quot;https://github.com/philss/rustler_precompiled&quot;&gt;&lt;code&gt;rustler_precompiled&lt;/code&gt;&lt;/a&gt; are working on a proper fix. You can follow the progress with &lt;a href=&quot;https://github.com/philss/rustler_precompiled/pull/113&quot;&gt;this pull request&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;&lt;a href=&quot;https://www.yellowduck.be/tags/development&quot;&gt;#development&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/http&quot;&gt;#http&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/elixir&quot;&gt;#elixir&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/terminal&quot;&gt;#terminal&lt;/a&gt;&lt;/p&gt;</content>
  </entry>
  <entry>
    <id>https://www.yellowduck.be/posts/streaming-zip-downloads-in-phoenix-with-zstream</id>
    <title>🐥 Streaming ZIP downloads in Phoenix with zstream</title>
    <updated>2026-10-23T17:00:00Z</updated>
    <published>2026-10-23T17:00:00Z</published>
    <author><name>Pieter Claerhout</name><email>pieter@yellowduck.be</email></author>
    <link rel="alternate" href="https://www.yellowduck.be/posts/streaming-zip-downloads-in-phoenix-with-zstream"/>
    <content type="html">&lt;p&gt;When you need to let users download multiple files as a ZIP in a Phoenix application, creating the archive upfront is often wasteful. It costs disk space, adds latency, and does not scale well for large files.&lt;/p&gt;
&lt;p&gt;A better approach is to &lt;strong&gt;stream the ZIP archive directly to the browser&lt;/strong&gt; while it's being generated. In Elixir, the &lt;code&gt;zstream&lt;/code&gt; library makes this straightforward and production-ready.&lt;/p&gt;
&lt;p&gt;This post shows how to stream a ZIP file from a Phoenix controller without creating temporary files.&lt;/p&gt;
&lt;h1&gt;The idea&lt;/h1&gt;
&lt;p&gt;The solution combines three pieces:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;Plug.Conn.send_chunked/2&lt;/code&gt; to start a streaming HTTP response&lt;/li&gt;
&lt;li&gt;&lt;code&gt;zstream&lt;/code&gt; to generate ZIP data incrementally&lt;/li&gt;
&lt;li&gt;File streams (&lt;code&gt;File.stream!/3&lt;/code&gt;) to avoid loading files into memory&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The browser starts downloading immediately, while the server produces ZIP chunks on the fly.&lt;/p&gt;
&lt;h1&gt;Adding the dependency&lt;/h1&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-elixir&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;span class=&quot;l-line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;l-keyword-function&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;l-function&quot;&gt;deps&lt;/span&gt; &lt;span class=&quot;l-keyword&quot;&gt;do&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;2&quot;&gt;  &lt;span class=&quot;l-punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;3&quot;&gt;    &lt;span class=&quot;l-punctuation-bracket&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;l-string-special-symbol&quot;&gt;:zstream&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;&amp;quot;~&amp;gt; 0.6&amp;quot;&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;4&quot;&gt;  &lt;span class=&quot;l-punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;5&quot;&gt;&lt;span class=&quot;l-keyword&quot;&gt;end&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;Streaming a ZIP from a controller&lt;/h1&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-elixir&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;span class=&quot;l-line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;l-keyword-function&quot;&gt;defmodule&lt;/span&gt; &lt;span class=&quot;l-module&quot;&gt;MyAppWeb.DownloadController&lt;/span&gt; &lt;span class=&quot;l-keyword-function&quot;&gt;do&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;2&quot;&gt;  &lt;span class=&quot;l-keyword&quot;&gt;use&lt;/span&gt; &lt;span class=&quot;l-module&quot;&gt;MyAppWeb&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-string-special-symbol&quot;&gt;:controller&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;3&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;4&quot;&gt;  &lt;span class=&quot;l-keyword-function&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;l-function&quot;&gt;images&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-variable&quot;&gt;conn&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-comment&quot;&gt;_params&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;l-keyword&quot;&gt;do&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;5&quot;&gt;    &lt;span class=&quot;l-variable&quot;&gt;files&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;l-punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;6&quot;&gt;      &lt;span class=&quot;l-punctuation-bracket&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;&amp;quot;image1.jpg&amp;quot;&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;&amp;quot;/data/images/image1.jpg&amp;quot;&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;7&quot;&gt;      &lt;span class=&quot;l-punctuation-bracket&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;&amp;quot;image2.png&amp;quot;&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;&amp;quot;/data/images/image2.png&amp;quot;&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;8&quot;&gt;    &lt;span class=&quot;l-punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;9&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;10&quot;&gt;    &lt;span class=&quot;l-variable&quot;&gt;conn&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;=&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;11&quot;&gt;      &lt;span class=&quot;l-variable&quot;&gt;conn&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;12&quot;&gt;      &lt;span class=&quot;l-operator&quot;&gt;|&amp;gt;&lt;/span&gt; &lt;span class=&quot;l-function-call&quot;&gt;put_resp_header&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;&amp;quot;content-type&amp;quot;&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;&amp;quot;application/zip&amp;quot;&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;13&quot;&gt;      &lt;span class=&quot;l-operator&quot;&gt;|&amp;gt;&lt;/span&gt; &lt;span class=&quot;l-function-call&quot;&gt;put_resp_header&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;14&quot;&gt;        &lt;span class=&quot;l-string&quot;&gt;&amp;quot;content-disposition&amp;quot;&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;15&quot;&gt;        &lt;span class=&quot;l-string&quot;&gt;&lt;span class=&quot;l-string&quot;&gt;~s(&lt;/span&gt;attachment; filename=&amp;quot;images.zip&amp;quot;&lt;span class=&quot;l-string&quot;&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;16&quot;&gt;      &lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;17&quot;&gt;      &lt;span class=&quot;l-operator&quot;&gt;|&amp;gt;&lt;/span&gt; &lt;span class=&quot;l-function-call&quot;&gt;send_chunked&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-number&quot;&gt;200&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;18&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;19&quot;&gt;    &lt;span class=&quot;l-variable&quot;&gt;zip_stream&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;=&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;20&quot;&gt;      &lt;span class=&quot;l-module&quot;&gt;Zstream&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;l-function-call&quot;&gt;zip&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;21&quot;&gt;        &lt;span class=&quot;l-module&quot;&gt;Enum&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;l-function-call&quot;&gt;map&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-variable&quot;&gt;files&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-keyword&quot;&gt;fn&lt;/span&gt; &lt;span class=&quot;l-punctuation-bracket&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;l-variable&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-variable&quot;&gt;path&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;22&quot;&gt;          &lt;span class=&quot;l-module&quot;&gt;Zstream&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;l-function-call&quot;&gt;entry&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-variable&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-module&quot;&gt;File&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;l-function-call&quot;&gt;stream!&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-variable&quot;&gt;path&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-punctuation-bracket&quot;&gt;[]&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-number&quot;&gt;64_000&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;))&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;23&quot;&gt;        &lt;span class=&quot;l-keyword&quot;&gt;end&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;24&quot;&gt;      &lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;25&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;26&quot;&gt;    &lt;span class=&quot;l-module&quot;&gt;Enum&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;l-function-call&quot;&gt;reduce_while&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-variable&quot;&gt;zip_stream&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-variable&quot;&gt;conn&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-keyword&quot;&gt;fn&lt;/span&gt; &lt;span class=&quot;l-variable&quot;&gt;chunk&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-variable&quot;&gt;conn&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;27&quot;&gt;      &lt;span class=&quot;l-keyword&quot;&gt;case&lt;/span&gt; &lt;span class=&quot;l-module&quot;&gt;Plug.Conn&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;l-function-call&quot;&gt;chunk&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-variable&quot;&gt;conn&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-variable&quot;&gt;chunk&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;l-keyword&quot;&gt;do&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;28&quot;&gt;        &lt;span class=&quot;l-punctuation-bracket&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;l-string-special-symbol&quot;&gt;:ok&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-variable&quot;&gt;conn&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&quot;l-punctuation-bracket&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;l-string-special-symbol&quot;&gt;:cont&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-variable&quot;&gt;conn&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;29&quot;&gt;        &lt;span class=&quot;l-punctuation-bracket&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;l-string-special-symbol&quot;&gt;:error&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-string-special-symbol&quot;&gt;:closed&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&quot;l-punctuation-bracket&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;l-string-special-symbol&quot;&gt;:halt&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-variable&quot;&gt;conn&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;30&quot;&gt;      &lt;span class=&quot;l-keyword&quot;&gt;end&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;31&quot;&gt;    &lt;span class=&quot;l-keyword&quot;&gt;end&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;32&quot;&gt;  &lt;span class=&quot;l-keyword&quot;&gt;end&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;33&quot;&gt;&lt;span class=&quot;l-keyword&quot;&gt;end&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;What's happening here:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Each file is represented as a &lt;code&gt;Zstream&lt;/code&gt; entry&lt;/li&gt;
&lt;li&gt;The file contents are streamed from disk&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Zstream.zip/1&lt;/code&gt; produces a stream of ZIP binary chunks&lt;/li&gt;
&lt;li&gt;Each chunk is forwarded to the client using &lt;code&gt;Plug.Conn.chunk/2&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;At no point is the full ZIP or any full file loaded into memory.&lt;/p&gt;
&lt;h1&gt;Adding folders to the ZIP&lt;/h1&gt;
&lt;p&gt;ZIP archives don't require explicit directory entries. Just use paths in the entry name:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-elixir&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;span class=&quot;l-line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;l-module&quot;&gt;Zstream&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;l-function-call&quot;&gt;entry&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;&amp;quot;images/image1.jpg&amp;quot;&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-module&quot;&gt;File&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;l-function-call&quot;&gt;stream!&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-variable&quot;&gt;path&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-punctuation-bracket&quot;&gt;[]&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-number&quot;&gt;64_000&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;))&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;Why this approach works well&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;No temporary ZIP files&lt;/li&gt;
&lt;li&gt;Constant memory usage&lt;/li&gt;
&lt;li&gt;Handles large files gracefully&lt;/li&gt;
&lt;li&gt;The download starts immediately in the browser&lt;/li&gt;
&lt;li&gt;Works with standard reverse proxies like Nginx or Caddy&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;Common pitfalls&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;Always call &lt;code&gt;send_chunked/2&lt;/code&gt; before streaming data&lt;/li&gt;
&lt;li&gt;Handle &lt;code&gt;{:error, :closed}&lt;/code&gt; when the client disconnects&lt;/li&gt;
&lt;li&gt;Use reasonably large chunk sizes (32–128 KB)&lt;/li&gt;
&lt;li&gt;Ensure file paths and names are trusted to avoid ZIP path traversal&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;Using this from LiveView&lt;/h1&gt;
&lt;p&gt;LiveView cannot stream responses itself. The usual pattern is to trigger a navigation to the controller action:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-elixir&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;span class=&quot;l-line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;l-function-call&quot;&gt;push_navigate&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-variable&quot;&gt;socket&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-string-special-symbol&quot;&gt;to: &lt;/span&gt;&lt;span class=&quot;l-string-special&quot;&gt;~p&amp;quot;&lt;/span&gt;/downloads/images&lt;span class=&quot;l-string-special&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The controller then handles the streaming ZIP download as shown above.&lt;/p&gt;
&lt;h1&gt;Closing thoughts&lt;/h1&gt;
&lt;p&gt;If you need to package many files into a ZIP download in Phoenix, &lt;code&gt;zstream&lt;/code&gt; is a simple and robust solution. It fits naturally with Plug's chunked responses and keeps resource usage predictable, even under load.&lt;/p&gt;
&lt;p&gt;For most applications, this is the cleanest way to offer ZIP downloads without touching the filesystem at all.&lt;/p&gt;&lt;p&gt;&lt;a href=&quot;https://www.yellowduck.be/tags/pattern&quot;&gt;#pattern&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/http&quot;&gt;#http&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/elixir&quot;&gt;#elixir&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/phoenix&quot;&gt;#phoenix&lt;/a&gt;&lt;/p&gt;</content>
  </entry>
  <entry>
    <id>https://www.yellowduck.be/posts/8-chunking-strategies-for-rag-based-agents</id>
    <title>🐥 8 Chunking strategies for RAG-based agents</title>
    <updated>2026-10-20T17:00:00Z</updated>
    <published>2026-10-20T17:00:00Z</published>
    <author><name>Pieter Claerhout</name><email>pieter@yellowduck.be</email></author>
    <link rel="alternate" href="https://www.yellowduck.be/posts/8-chunking-strategies-for-rag-based-agents"/>
    <content type="html">&lt;p&gt;&lt;img src=&quot;https://www.yellowduck.be/media/88462ca4-b0a1-4441-9537-59dae9d5ef65_2484x3002.webp&quot; alt=&quot;8 Chunking strategies for RAG-based agents&quot; /&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Fixed-Si&lt;/strong&gt;ze: cuts text every fixed number of tokens, with a small overlap.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Recursive&lt;/strong&gt;: splits on paragraphs first, falling back to lines then words and characters only when a piece is still too big.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Document-Based&lt;/strong&gt;: splits wherever the document already marks a break, a heading, a chapter, an HTML tag, so a markdown file becomes one chunk per section on its own.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Semantic&lt;/strong&gt;: embeds each sentence, checks its similarity to the one before it, and starts a new chunk when that similarity drops. This finds real topic shifts instead of guessing by length.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;LLM-Based&lt;/strong&gt;: asks an LLM to rewrite the text as standalone propositions, one idea per line, and turns each into a chunk, high quality but expensive across a large corpus.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Agentic&lt;/strong&gt;: an AI agent reads each document, decides what it is (code, contract, table), and picks the chunking method for that file instead of one rule for everything.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Late Chunking&lt;/strong&gt;: runs the full document through an embedding model, then applies normal split boundaries and mean-pools within each boundary to create chunk embeddings.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Hierarchical&lt;/strong&gt;: breaks the document into layers, sections, paragraphs, then sentences&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Courtesy of &lt;a href=&quot;https://newsletter.bytebytego.com/authors/bytebytego&quot;&gt;the ByteByteGo newsletter&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;&lt;a href=&quot;https://www.yellowduck.be/tags/ai&quot;&gt;#ai&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/langchain&quot;&gt;#langchain&lt;/a&gt;&lt;/p&gt;</content>
  </entry>
  <entry>
    <id>https://www.yellowduck.be/posts/translating-country-codes-to-full-names-in-elixir-using-ex-cldr-countries</id>
    <title>🐥 Translating country codes to full names in Elixir using `ex_cldr_countries`</title>
    <updated>2026-10-18T17:00:00Z</updated>
    <published>2026-10-18T17:00:00Z</published>
    <author><name>Pieter Claerhout</name><email>pieter@yellowduck.be</email></author>
    <link rel="alternate" href="https://www.yellowduck.be/posts/translating-country-codes-to-full-names-in-elixir-using-ex-cldr-countries"/>
    <content type="html">&lt;p&gt;Unlike JavaScript's &lt;code&gt;Intl.DisplayNames&lt;/code&gt;, Elixir doesn't provide built-in localization for country names. However, with the help of the excellent &lt;a href=&quot;https://hexdocs.pm/ex_cldr_countries/readme.html&quot;&gt;&lt;code&gt;ex_cldr_countries&lt;/code&gt;&lt;/a&gt; package, you can easily achieve dynamic, localized country translations backed by official Unicode CLDR data.&lt;/p&gt;
&lt;h1&gt;Installation&lt;/h1&gt;
&lt;p&gt;Add the package to your &lt;code&gt;mix.exs&lt;/code&gt;:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-elixir&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;span class=&quot;l-line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;l-keyword-function&quot;&gt;defp&lt;/span&gt; &lt;span class=&quot;l-function&quot;&gt;deps&lt;/span&gt; &lt;span class=&quot;l-keyword&quot;&gt;do&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;2&quot;&gt;  &lt;span class=&quot;l-punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;3&quot;&gt;    &lt;span class=&quot;l-punctuation-bracket&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;l-string-special-symbol&quot;&gt;:ex_cldr_countries&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;&amp;quot;~&amp;gt; 2.0&amp;quot;&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;4&quot;&gt;  &lt;span class=&quot;l-punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;5&quot;&gt;&lt;span class=&quot;l-keyword&quot;&gt;end&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then fetch it:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-bash&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;span class=&quot;l-line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;l-function-call&quot;&gt;mix&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;deps.get&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;Setting Up a Backend&lt;/h1&gt;
&lt;p&gt;Most examples online show calling &lt;code&gt;Cldr.Countries.from_iso/2&lt;/code&gt; directly, but modern &lt;code&gt;ex_cldr&lt;/code&gt; usage requires a &lt;strong&gt;backend module&lt;/strong&gt;. Without one, you're relying on a default backend that may not be configured — and you lose control over which locales get compiled in.&lt;/p&gt;
&lt;p&gt;Create a backend module in your app:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-elixir&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;span class=&quot;l-line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;l-comment&quot;&gt;# lib/my_app/cldr.ex&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;2&quot;&gt;&lt;span class=&quot;l-keyword-function&quot;&gt;defmodule&lt;/span&gt; &lt;span class=&quot;l-module&quot;&gt;MyApp.Cldr&lt;/span&gt; &lt;span class=&quot;l-keyword-function&quot;&gt;do&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;3&quot;&gt;  &lt;span class=&quot;l-keyword&quot;&gt;use&lt;/span&gt; &lt;span class=&quot;l-module&quot;&gt;Cldr&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;4&quot;&gt;    &lt;span class=&quot;l-string-special-symbol&quot;&gt;locales: &lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;&amp;quot;en&amp;quot;&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;&amp;quot;fr&amp;quot;&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;&amp;quot;nl&amp;quot;&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;5&quot;&gt;    &lt;span class=&quot;l-string-special-symbol&quot;&gt;default_locale: &lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;&amp;quot;en&amp;quot;&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;6&quot;&gt;    &lt;span class=&quot;l-string-special-symbol&quot;&gt;providers: &lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;l-module&quot;&gt;Cldr.Countries&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;7&quot;&gt;&lt;span class=&quot;l-keyword&quot;&gt;end&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;locales&lt;/code&gt; list determines which translations get compiled into your app. Keep it to what you actually need — CLDR data is large and including everything will bloat your build.&lt;/p&gt;
&lt;h1&gt;Translating Country Codes&lt;/h1&gt;
&lt;p&gt;With the backend in place, translating a country code is straightforward:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-elixir&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;span class=&quot;l-line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;l-module&quot;&gt;MyApp.Cldr.Countries&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;l-function-call&quot;&gt;from_iso&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-string-special-symbol&quot;&gt;:BE&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-string-special-symbol&quot;&gt;locale: &lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;&amp;quot;fr&amp;quot;&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;2&quot;&gt;&lt;span class=&quot;l-comment&quot;&gt;# =&amp;gt; {:ok, &amp;quot;Belgique&amp;quot;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;3&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;4&quot;&gt;&lt;span class=&quot;l-module&quot;&gt;MyApp.Cldr.Countries&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;l-function-call&quot;&gt;from_iso&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-string-special-symbol&quot;&gt;:NL&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-string-special-symbol&quot;&gt;locale: &lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;&amp;quot;en&amp;quot;&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;5&quot;&gt;&lt;span class=&quot;l-comment&quot;&gt;# =&amp;gt; {:ok, &amp;quot;Netherlands&amp;quot;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;6&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;7&quot;&gt;&lt;span class=&quot;l-module&quot;&gt;MyApp.Cldr.Countries&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;l-function-call&quot;&gt;from_iso&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-string-special-symbol&quot;&gt;:DE&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-string-special-symbol&quot;&gt;locale: &lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;&amp;quot;nl&amp;quot;&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;8&quot;&gt;&lt;span class=&quot;l-comment&quot;&gt;# =&amp;gt; {:ok, &amp;quot;Duitsland&amp;quot;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;The first argument is the ISO 3166-1 Alpha-2 code as an atom (&lt;code&gt;:BE&lt;/code&gt;, &lt;code&gt;:NL&lt;/code&gt;, &lt;code&gt;:DE&lt;/code&gt;, …)&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;locale&lt;/code&gt; option sets the output language&lt;/li&gt;
&lt;li&gt;If the translation can't be found, it returns an &lt;code&gt;{:error, reason}&lt;/code&gt; tuple, so it's safe to pattern match on&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;Why This Approach&lt;/h1&gt;
&lt;p&gt;The backend module gives you three concrete benefits:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Scoped locale compilation&lt;/strong&gt; — only the locales you list get compiled, keeping your build lean&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Explicit and testable&lt;/strong&gt; — the backend is a real module you can reference directly in tests&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;No global state surprises&lt;/strong&gt; — the bare &lt;code&gt;Cldr.*&lt;/code&gt; namespace is just a shim that delegates to whichever backend is configured as the default; using your own backend is always more predictable&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;For most Phoenix applications, dropping this into a &lt;code&gt;lib/my_app/cldr.ex&lt;/code&gt; file is all the setup you need.&lt;/p&gt;&lt;p&gt;&lt;a href=&quot;https://www.yellowduck.be/tags/development&quot;&gt;#development&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/best-practice&quot;&gt;#best-practice&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/elixir&quot;&gt;#elixir&lt;/a&gt;&lt;/p&gt;</content>
  </entry>
  <entry>
    <id>https://www.yellowduck.be/posts/handling-local-timezones-in-ash-while-storing-utc-datetimes</id>
    <title>🐥 Handling local timezones in Ash while storing UTC datetimes</title>
    <updated>2026-10-14T17:00:00Z</updated>
    <published>2026-10-14T17:00:00Z</published>
    <author><name>Pieter Claerhout</name><email>pieter@yellowduck.be</email></author>
    <link rel="alternate" href="https://www.yellowduck.be/posts/handling-local-timezones-in-ash-while-storing-utc-datetimes"/>
    <content type="html">&lt;p&gt;When building Elixir applications with the Ash Framework, it's a best practice to store all datetimes in UTC in your database. However, users typically interact with your application in their &lt;strong&gt;local timezone&lt;/strong&gt;, so you need a reliable way to convert their input into UTC—and show it back in their local time.&lt;/p&gt;
&lt;p&gt;This post shows how to handle that conversion cleanly and reusably in Ash actions using a custom &lt;code&gt;change&lt;/code&gt; module.&lt;/p&gt;
&lt;h1&gt;Why store in UTC?&lt;/h1&gt;
&lt;p&gt;Storing all datetimes in UTC avoids timezone ambiguity and makes it easier to compare, sort, and query times consistently—especially if your users are spread across multiple regions. PostgreSQL and Ash support UTC datetime types (&lt;code&gt;:utc_datetime&lt;/code&gt;), and your application should convert values at the boundaries: when receiving input or displaying output.&lt;/p&gt;
&lt;h1&gt;Define UTC datetime attributes&lt;/h1&gt;
&lt;p&gt;First, define the attributes on your Ash resource using &lt;code&gt;:utc_datetime&lt;/code&gt;:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-elixir&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;span class=&quot;l-line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;l-function-call&quot;&gt;attributes&lt;/span&gt; &lt;span class=&quot;l-keyword&quot;&gt;do&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;2&quot;&gt;  &lt;span class=&quot;l-function-call&quot;&gt;attribute&lt;/span&gt; &lt;span class=&quot;l-string-special-symbol&quot;&gt;:starts_at&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-string-special-symbol&quot;&gt;:utc_datetime&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;3&quot;&gt;  &lt;span class=&quot;l-function-call&quot;&gt;attribute&lt;/span&gt; &lt;span class=&quot;l-string-special-symbol&quot;&gt;:ends_at&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-string-special-symbol&quot;&gt;:utc_datetime&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;4&quot;&gt;&lt;span class=&quot;l-keyword&quot;&gt;end&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;Add user-local input arguments&lt;/h1&gt;
&lt;p&gt;In your Ash action, accept user-local datetime input as strings, &lt;code&gt;NaiveDateTime&lt;/code&gt;, or &lt;code&gt;DateTime&lt;/code&gt; structs:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-elixir&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;span class=&quot;l-line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;l-function-call&quot;&gt;actions&lt;/span&gt; &lt;span class=&quot;l-keyword&quot;&gt;do&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;2&quot;&gt;  &lt;span class=&quot;l-function-call&quot;&gt;create&lt;/span&gt; &lt;span class=&quot;l-string-special-symbol&quot;&gt;:schedule&lt;/span&gt; &lt;span class=&quot;l-keyword&quot;&gt;do&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;3&quot;&gt;    &lt;span class=&quot;l-function-call&quot;&gt;argument&lt;/span&gt; &lt;span class=&quot;l-string-special-symbol&quot;&gt;:starts_at_local&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-string-special-symbol&quot;&gt;:string&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;4&quot;&gt;    &lt;span class=&quot;l-function-call&quot;&gt;argument&lt;/span&gt; &lt;span class=&quot;l-string-special-symbol&quot;&gt;:ends_at_local&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-string-special-symbol&quot;&gt;:naive_datetime&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;5&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;6&quot;&gt;    &lt;span class=&quot;l-function-call&quot;&gt;change&lt;/span&gt; &lt;span class=&quot;l-punctuation-bracket&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;l-module&quot;&gt;MyApp.Changes.ConvertLocalDatetimeToUtc&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-string-special-symbol&quot;&gt;arg_key: :starts_at_local&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-string-special-symbol&quot;&gt;field: :starts_at&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;7&quot;&gt;    &lt;span class=&quot;l-function-call&quot;&gt;change&lt;/span&gt; &lt;span class=&quot;l-punctuation-bracket&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;l-module&quot;&gt;MyApp.Changes.ConvertLocalDatetimeToUtc&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-string-special-symbol&quot;&gt;arg_key: :ends_at_local&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-string-special-symbol&quot;&gt;field: :ends_at&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;8&quot;&gt;  &lt;span class=&quot;l-keyword&quot;&gt;end&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;9&quot;&gt;&lt;span class=&quot;l-keyword&quot;&gt;end&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;Reusable change module&lt;/h1&gt;
&lt;p&gt;Ash's &lt;code&gt;change&lt;/code&gt; DSL expects a module implementing &lt;code&gt;Ash.Resource.Change&lt;/code&gt;. Here's a reusable implementation you can drop into your project:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-elixir&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;span class=&quot;l-line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;l-keyword-function&quot;&gt;defmodule&lt;/span&gt; &lt;span class=&quot;l-module&quot;&gt;MyApp.Changes.ConvertLocalDatetimeToUtc&lt;/span&gt; &lt;span class=&quot;l-keyword-function&quot;&gt;do&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;2&quot;&gt;  &lt;span class=&quot;l-operator&quot;&gt;&lt;span class=&quot;l-comment-documentation&quot;&gt;&lt;span class=&quot;l-comment&quot;&gt;@moduledoc&lt;/span&gt; &lt;span class=&quot;l-comment&quot;&gt;&amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;3&quot;&gt;&lt;span class=&quot;l-operator&quot;&gt;&lt;span class=&quot;l-comment-documentation&quot;&gt;&lt;span class=&quot;l-comment&quot;&gt;  Converts a local datetime argument (string, NaiveDateTime, or DateTime)&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;4&quot;&gt;&lt;span class=&quot;l-operator&quot;&gt;&lt;span class=&quot;l-comment-documentation&quot;&gt;&lt;span class=&quot;l-comment&quot;&gt;  to UTC and assigns it to an Ash attribute. Gracefully handles &lt;span class=&quot;l-markup-raw&quot;&gt;`nil`&lt;/span&gt; values.&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;5&quot;&gt;&lt;span class=&quot;l-operator&quot;&gt;&lt;span class=&quot;l-comment-documentation&quot;&gt;&lt;span class=&quot;l-comment&quot;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;6&quot;&gt;&lt;span class=&quot;l-operator&quot;&gt;&lt;span class=&quot;l-comment-documentation&quot;&gt;&lt;span class=&quot;l-comment&quot;&gt;  Options:&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;7&quot;&gt;&lt;span class=&quot;l-operator&quot;&gt;&lt;span class=&quot;l-comment-documentation&quot;&gt;&lt;span class=&quot;l-comment&quot;&gt;    - &lt;span class=&quot;l-markup-raw&quot;&gt;`:arg_key`&lt;/span&gt; — the action argument holding the local datetime value&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;8&quot;&gt;&lt;span class=&quot;l-operator&quot;&gt;&lt;span class=&quot;l-comment-documentation&quot;&gt;&lt;span class=&quot;l-comment&quot;&gt;    - &lt;span class=&quot;l-markup-raw&quot;&gt;`:field`&lt;/span&gt; — the resource attribute to assign the UTC value to&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;9&quot;&gt;&lt;span class=&quot;l-operator&quot;&gt;&lt;span class=&quot;l-comment-documentation&quot;&gt;&lt;span class=&quot;l-comment&quot;&gt;    - &lt;span class=&quot;l-markup-raw&quot;&gt;`:timezone_key`&lt;/span&gt; — the context key for the user&amp;#39;s timezone (default: &lt;span class=&quot;l-markup-raw&quot;&gt;`:user_timezone`&lt;/span&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;10&quot;&gt;&lt;span class=&quot;l-operator&quot;&gt;&lt;span class=&quot;l-comment-documentation&quot;&gt;&lt;span class=&quot;l-comment&quot;&gt;  &amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;11&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;12&quot;&gt;  &lt;span class=&quot;l-keyword&quot;&gt;use&lt;/span&gt; &lt;span class=&quot;l-module&quot;&gt;Ash.Resource.Change&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;13&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;14&quot;&gt;  &lt;span class=&quot;l-keyword-function&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;l-function&quot;&gt;init&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-variable&quot;&gt;opts&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;l-keyword&quot;&gt;do&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;15&quot;&gt;    &lt;span class=&quot;l-variable&quot;&gt;opts&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;l-module&quot;&gt;Keyword&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;l-function-call&quot;&gt;put_new&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-variable&quot;&gt;opts&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-string-special-symbol&quot;&gt;:timezone_key&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-string-special-symbol&quot;&gt;:user_timezone&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;16&quot;&gt;    &lt;span class=&quot;l-punctuation-bracket&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;l-string-special-symbol&quot;&gt;:ok&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-variable&quot;&gt;opts&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;17&quot;&gt;  &lt;span class=&quot;l-keyword&quot;&gt;end&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;18&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;19&quot;&gt;  &lt;span class=&quot;l-keyword-function&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;l-function&quot;&gt;change&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-variable&quot;&gt;changeset&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-variable&quot;&gt;opts&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-comment&quot;&gt;_context&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;l-keyword&quot;&gt;do&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;20&quot;&gt;    &lt;span class=&quot;l-variable&quot;&gt;arg_key&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;l-variable&quot;&gt;opts&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;l-string-special-symbol&quot;&gt;:arg_key&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;21&quot;&gt;    &lt;span class=&quot;l-variable&quot;&gt;field&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;l-variable&quot;&gt;opts&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;l-string-special-symbol&quot;&gt;:field&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;22&quot;&gt;    &lt;span class=&quot;l-variable&quot;&gt;timezone_key&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;l-variable&quot;&gt;opts&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;l-string-special-symbol&quot;&gt;:timezone_key&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;23&quot;&gt;    &lt;span class=&quot;l-variable&quot;&gt;timezone&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;l-module&quot;&gt;Ash.Changeset&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;l-function-call&quot;&gt;get_context&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-variable&quot;&gt;changeset&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-variable&quot;&gt;timezone_key&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;||&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;&amp;quot;UTC&amp;quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;24&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;25&quot;&gt;    &lt;span class=&quot;l-keyword&quot;&gt;case&lt;/span&gt; &lt;span class=&quot;l-module&quot;&gt;Ash.Changeset&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;l-function-call&quot;&gt;fetch_argument&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-variable&quot;&gt;changeset&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-variable&quot;&gt;arg_key&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;l-keyword&quot;&gt;do&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;26&quot;&gt;      &lt;span class=&quot;l-punctuation-bracket&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;l-string-special-symbol&quot;&gt;:ok&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-constant-builtin&quot;&gt;nil&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;27&quot;&gt;        &lt;span class=&quot;l-variable&quot;&gt;changeset&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;28&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;29&quot;&gt;      &lt;span class=&quot;l-punctuation-bracket&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;l-string-special-symbol&quot;&gt;:ok&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-variable&quot;&gt;value&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;30&quot;&gt;        &lt;span class=&quot;l-keyword&quot;&gt;case&lt;/span&gt; &lt;span class=&quot;l-function-call&quot;&gt;do_convert_to_utc&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-variable&quot;&gt;value&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-variable&quot;&gt;timezone&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;l-keyword&quot;&gt;do&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;31&quot;&gt;          &lt;span class=&quot;l-punctuation-bracket&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;l-string-special-symbol&quot;&gt;:ok&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-variable&quot;&gt;utc_dt&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;32&quot;&gt;            &lt;span class=&quot;l-module&quot;&gt;Ash.Changeset&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;l-function-call&quot;&gt;force_change_attribute&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-variable&quot;&gt;changeset&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-variable&quot;&gt;field&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-variable&quot;&gt;utc_dt&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;33&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;34&quot;&gt;          &lt;span class=&quot;l-comment&quot;&gt;_&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;35&quot;&gt;            &lt;span class=&quot;l-module&quot;&gt;Ash.Changeset&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;l-function-call&quot;&gt;add_error&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;36&quot;&gt;              &lt;span class=&quot;l-variable&quot;&gt;changeset&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;37&quot;&gt;              &lt;span class=&quot;l-string&quot;&gt;&amp;quot;&lt;span class=&quot;l-string-special&quot;&gt;#{&lt;/span&gt;&lt;span class=&quot;l-variable&quot;&gt;arg_key&lt;/span&gt;&lt;span class=&quot;l-string-special&quot;&gt;}&lt;/span&gt; is invalid or could not be converted to UTC&amp;quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;38&quot;&gt;            &lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;39&quot;&gt;        &lt;span class=&quot;l-keyword&quot;&gt;end&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;40&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;41&quot;&gt;      &lt;span class=&quot;l-string-special-symbol&quot;&gt;:error&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;42&quot;&gt;        &lt;span class=&quot;l-variable&quot;&gt;changeset&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;43&quot;&gt;    &lt;span class=&quot;l-keyword&quot;&gt;end&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;44&quot;&gt;  &lt;span class=&quot;l-keyword&quot;&gt;end&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;45&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;46&quot;&gt;  &lt;span class=&quot;l-keyword-function&quot;&gt;defp&lt;/span&gt; &lt;span class=&quot;l-function&quot;&gt;do_convert_to_utc&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-constant-builtin&quot;&gt;nil&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-comment&quot;&gt;_&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-string-special-symbol&quot;&gt;do: &lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;l-string-special-symbol&quot;&gt;:ok&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-constant-builtin&quot;&gt;nil&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;47&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;48&quot;&gt;  &lt;span class=&quot;l-keyword-function&quot;&gt;defp&lt;/span&gt; &lt;span class=&quot;l-function&quot;&gt;do_convert_to_utc&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-punctuation-special&quot;&gt;%&lt;/span&gt;&lt;span class=&quot;l-module&quot;&gt;DateTime&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;{}&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;l-variable&quot;&gt;dt&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-comment&quot;&gt;_timezone&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;l-keyword&quot;&gt;do&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;49&quot;&gt;    &lt;span class=&quot;l-module&quot;&gt;DateTime&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;l-function-call&quot;&gt;shift_zone&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-variable&quot;&gt;dt&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;&amp;quot;Etc/UTC&amp;quot;&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;50&quot;&gt;  &lt;span class=&quot;l-keyword&quot;&gt;end&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;51&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;52&quot;&gt;  &lt;span class=&quot;l-keyword-function&quot;&gt;defp&lt;/span&gt; &lt;span class=&quot;l-function&quot;&gt;do_convert_to_utc&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-punctuation-special&quot;&gt;%&lt;/span&gt;&lt;span class=&quot;l-module&quot;&gt;NaiveDateTime&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;{}&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;l-variable&quot;&gt;naive&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-variable&quot;&gt;timezone&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;l-keyword&quot;&gt;do&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;53&quot;&gt;    &lt;span class=&quot;l-keyword&quot;&gt;with&lt;/span&gt; &lt;span class=&quot;l-punctuation-bracket&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;l-string-special-symbol&quot;&gt;:ok&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-variable&quot;&gt;dt_local&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;&amp;lt;-&lt;/span&gt; &lt;span class=&quot;l-module&quot;&gt;DateTime&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;l-function-call&quot;&gt;from_naive&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-variable&quot;&gt;naive&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-variable&quot;&gt;timezone&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;54&quot;&gt;         &lt;span class=&quot;l-punctuation-bracket&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;l-string-special-symbol&quot;&gt;:ok&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-variable&quot;&gt;dt_utc&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;&amp;lt;-&lt;/span&gt; &lt;span class=&quot;l-module&quot;&gt;DateTime&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;l-function-call&quot;&gt;shift_zone&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-variable&quot;&gt;dt_local&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;&amp;quot;Etc/UTC&amp;quot;&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;l-keyword&quot;&gt;do&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;55&quot;&gt;      &lt;span class=&quot;l-punctuation-bracket&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;l-string-special-symbol&quot;&gt;:ok&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-variable&quot;&gt;dt_utc&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;56&quot;&gt;    &lt;span class=&quot;l-keyword&quot;&gt;end&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;57&quot;&gt;  &lt;span class=&quot;l-keyword&quot;&gt;end&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;58&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;59&quot;&gt;  &lt;span class=&quot;l-keyword-function&quot;&gt;defp&lt;/span&gt; &lt;span class=&quot;l-function&quot;&gt;do_convert_to_utc&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-variable&quot;&gt;value&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-variable&quot;&gt;timezone&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;l-keyword&quot;&gt;when&lt;/span&gt; &lt;span class=&quot;l-function-call&quot;&gt;is_binary&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-variable&quot;&gt;value&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;l-keyword&quot;&gt;do&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;60&quot;&gt;    &lt;span class=&quot;l-keyword&quot;&gt;with&lt;/span&gt; &lt;span class=&quot;l-punctuation-bracket&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;l-string-special-symbol&quot;&gt;:ok&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-variable&quot;&gt;naive&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;&amp;lt;-&lt;/span&gt; &lt;span class=&quot;l-module&quot;&gt;NaiveDateTime&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;l-function-call&quot;&gt;from_iso8601&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-variable&quot;&gt;value&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;61&quot;&gt;         &lt;span class=&quot;l-punctuation-bracket&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;l-string-special-symbol&quot;&gt;:ok&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-variable&quot;&gt;dt_local&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;&amp;lt;-&lt;/span&gt; &lt;span class=&quot;l-module&quot;&gt;DateTime&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;l-function-call&quot;&gt;from_naive&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-variable&quot;&gt;naive&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-variable&quot;&gt;timezone&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;62&quot;&gt;         &lt;span class=&quot;l-punctuation-bracket&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;l-string-special-symbol&quot;&gt;:ok&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-variable&quot;&gt;dt_utc&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;&amp;lt;-&lt;/span&gt; &lt;span class=&quot;l-module&quot;&gt;DateTime&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;l-function-call&quot;&gt;shift_zone&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-variable&quot;&gt;dt_local&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;&amp;quot;Etc/UTC&amp;quot;&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;l-keyword&quot;&gt;do&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;63&quot;&gt;      &lt;span class=&quot;l-punctuation-bracket&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;l-string-special-symbol&quot;&gt;:ok&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-variable&quot;&gt;dt_utc&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;64&quot;&gt;    &lt;span class=&quot;l-keyword&quot;&gt;end&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;65&quot;&gt;  &lt;span class=&quot;l-keyword&quot;&gt;end&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;66&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;67&quot;&gt;  &lt;span class=&quot;l-keyword-function&quot;&gt;defp&lt;/span&gt; &lt;span class=&quot;l-function&quot;&gt;do_convert_to_utc&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-comment&quot;&gt;_&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-comment&quot;&gt;_&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-string-special-symbol&quot;&gt;do: &lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;l-string-special-symbol&quot;&gt;:error&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-string-special-symbol&quot;&gt;:invalid&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;68&quot;&gt;&lt;span class=&quot;l-keyword&quot;&gt;end&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; &lt;code&gt;DateTime.shift_zone/2&lt;/code&gt; requires a timezone database. Add &lt;code&gt;{:tzdata, &amp;quot;~&amp;gt; 1.1&amp;quot;}&lt;/code&gt; to your &lt;code&gt;mix.exs&lt;/code&gt; dependencies and configure it:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-elixir&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;span class=&quot;l-line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;l-function-call&quot;&gt;config&lt;/span&gt; &lt;span class=&quot;l-string-special-symbol&quot;&gt;:elixir&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-string-special-symbol&quot;&gt;:time_zone_database&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-module&quot;&gt;Tzdata.TimeZoneDatabase&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/blockquote&gt;
&lt;h1&gt;Setting the timezone in context&lt;/h1&gt;
&lt;p&gt;To apply the conversion correctly, the user's timezone should be passed in the Ash changeset context.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Ash 3.x (domain-based):&lt;/strong&gt;&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-elixir&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;span class=&quot;l-line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;l-module&quot;&gt;AshPhoenix.Form&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;l-function-call&quot;&gt;for_action&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-module&quot;&gt;MyApp.Event&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-string-special-symbol&quot;&gt;:schedule&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;2&quot;&gt;  &lt;span class=&quot;l-string-special-symbol&quot;&gt;domain: &lt;/span&gt;&lt;span class=&quot;l-module&quot;&gt;MyApp.Domain&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;3&quot;&gt;  &lt;span class=&quot;l-string-special-symbol&quot;&gt;as: :event&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;4&quot;&gt;  &lt;span class=&quot;l-string-special-symbol&quot;&gt;context: &lt;/span&gt;&lt;span class=&quot;l-punctuation-special&quot;&gt;%&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;l-string-special-symbol&quot;&gt;user_timezone: &lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;&amp;quot;Europe/Brussels&amp;quot;&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;5&quot;&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Ash 2.x (api-based):&lt;/strong&gt;&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-elixir&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;span class=&quot;l-line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;l-module&quot;&gt;AshPhoenix.Form&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;l-function-call&quot;&gt;for_action&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-module&quot;&gt;MyApp.Event&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-string-special-symbol&quot;&gt;:schedule&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;2&quot;&gt;  &lt;span class=&quot;l-string-special-symbol&quot;&gt;api: &lt;/span&gt;&lt;span class=&quot;l-module&quot;&gt;MyApp.Api&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;3&quot;&gt;  &lt;span class=&quot;l-string-special-symbol&quot;&gt;as: :event&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;4&quot;&gt;  &lt;span class=&quot;l-string-special-symbol&quot;&gt;context: &lt;/span&gt;&lt;span class=&quot;l-punctuation-special&quot;&gt;%&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;l-string-special-symbol&quot;&gt;user_timezone: &lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;&amp;quot;Europe/Brussels&amp;quot;&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;5&quot;&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;Summary&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;Store all datetime attributes in UTC.&lt;/li&gt;
&lt;li&gt;Accept user-local input via custom &lt;code&gt;argument&lt;/code&gt;s.&lt;/li&gt;
&lt;li&gt;Use a reusable &lt;code&gt;Ash.Resource.Change&lt;/code&gt; module to convert values to UTC.&lt;/li&gt;
&lt;li&gt;Pass the user's timezone via &lt;code&gt;context&lt;/code&gt; on the changeset.&lt;/li&gt;
&lt;li&gt;Handle strings, &lt;code&gt;NaiveDateTime&lt;/code&gt;, &lt;code&gt;DateTime&lt;/code&gt;, and &lt;code&gt;nil&lt;/code&gt; values gracefully.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This pattern keeps your Ash resources clean and your time handling robust across different timezones.&lt;/p&gt;&lt;p&gt;&lt;a href=&quot;https://www.yellowduck.be/tags/database&quot;&gt;#database&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/elixir&quot;&gt;#elixir&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/phoenix&quot;&gt;#phoenix&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/postgresql&quot;&gt;#postgresql&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/sql&quot;&gt;#sql&lt;/a&gt;&lt;/p&gt;</content>
  </entry>
  <entry>
    <id>https://www.yellowduck.be/posts/building-a-lift-scheduling-algorithm-in-python</id>
    <title>🐥 Building a lift scheduling algorithm in Python</title>
    <updated>2026-10-11T17:00:00Z</updated>
    <published>2026-10-11T17:00:00Z</published>
    <author><name>Pieter Claerhout</name><email>pieter@yellowduck.be</email></author>
    <link rel="alternate" href="https://www.yellowduck.be/posts/building-a-lift-scheduling-algorithm-in-python"/>
    <content type="html">&lt;p&gt;A lift looks simple from the outside: press a button, wait, and eventually it arrives. Internally, however, a lift controller is a scheduling problem.&lt;/p&gt;
&lt;p&gt;With multiple lifts and multiple passengers, the controller needs to decide which lift should handle which request, while trying to minimise waiting time, travel time, and unnecessary stops.&lt;/p&gt;
&lt;p&gt;Let's build a simplified version in Python.&lt;/p&gt;
&lt;h1&gt;Start with a single lift&lt;/h1&gt;
&lt;p&gt;A lift needs to keep track of its current state:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-python&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;span class=&quot;l-line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;l-keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;l-variable&quot;&gt;dataclasses&lt;/span&gt; &lt;span class=&quot;l-keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;l-variable&quot;&gt;dataclass&lt;/span&gt;, &lt;span class=&quot;l-variable&quot;&gt;field&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;2&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;3&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;4&quot;&gt;&lt;span class=&quot;l-function&quot;&gt;@&lt;span class=&quot;l-function&quot;&gt;dataclass&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;5&quot;&gt;&lt;span class=&quot;l-keyword&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;l-constructor&quot;&gt;Lift&lt;/span&gt;:&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;6&quot;&gt;    &lt;span class=&quot;l-variable&quot;&gt;floor&lt;/span&gt;: &lt;span class=&quot;l-type&quot;&gt;int&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;7&quot;&gt;    &lt;span class=&quot;l-variable&quot;&gt;direction&lt;/span&gt;: &lt;span class=&quot;l-variable&quot;&gt;str&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;l-constant-builtin&quot;&gt;None&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;l-constant-builtin&quot;&gt;None&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;8&quot;&gt;    &lt;span class=&quot;l-variable&quot;&gt;targets&lt;/span&gt;: &lt;span class=&quot;l-variable&quot;&gt;set&lt;/span&gt;[&lt;span class=&quot;l-type&quot;&gt;int&lt;/span&gt;] &lt;span class=&quot;l-operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;l-function&quot;&gt;field&lt;/span&gt;(&lt;span class=&quot;l-variable&quot;&gt;default_factory&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;l-variable&quot;&gt;set&lt;/span&gt;)&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A simple controller can use the &lt;strong&gt;LOOK scheduling algorithm&lt;/strong&gt; to select the next floor.&lt;/p&gt;
&lt;p&gt;LOOK works like this:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Continue in the current direction while there are targets in that direction.&lt;/li&gt;
&lt;li&gt;Visit the closest target in that direction.&lt;/li&gt;
&lt;li&gt;When there are no more targets in that direction, reverse.&lt;/li&gt;
&lt;li&gt;If there are no targets at all, become idle.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-python&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;span class=&quot;l-line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;l-keyword&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;l-function&quot;&gt;next_target&lt;/span&gt;(&lt;span class=&quot;l-variable&quot;&gt;lift&lt;/span&gt;: &lt;span class=&quot;l-type&quot;&gt;Lift&lt;/span&gt;) &lt;span class=&quot;l-operator&quot;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&quot;l-variable&quot;&gt;int&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;l-constant-builtin&quot;&gt;None&lt;/span&gt;:&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;2&quot;&gt;    &lt;span class=&quot;l-keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;not&lt;/span&gt; &lt;span class=&quot;l-variable&quot;&gt;lift&lt;/span&gt;.&lt;span class=&quot;l-property&quot;&gt;targets&lt;/span&gt;:&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;3&quot;&gt;        &lt;span class=&quot;l-variable&quot;&gt;lift&lt;/span&gt;.&lt;span class=&quot;l-property&quot;&gt;direction&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;l-constant-builtin&quot;&gt;None&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;4&quot;&gt;        &lt;span class=&quot;l-keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;l-constant-builtin&quot;&gt;None&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;5&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;6&quot;&gt;    &lt;span class=&quot;l-keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;l-variable&quot;&gt;lift&lt;/span&gt;.&lt;span class=&quot;l-property&quot;&gt;direction&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;is&lt;/span&gt; &lt;span class=&quot;l-constant-builtin&quot;&gt;None&lt;/span&gt;:&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;7&quot;&gt;        &lt;span class=&quot;l-variable&quot;&gt;lift&lt;/span&gt;.&lt;span class=&quot;l-property&quot;&gt;direction&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;=&lt;/span&gt; (&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;8&quot;&gt;            &lt;span class=&quot;l-string&quot;&gt;&amp;quot;up&amp;quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;9&quot;&gt;            &lt;span class=&quot;l-keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;l-function-builtin&quot;&gt;min&lt;/span&gt;(&lt;span class=&quot;l-variable&quot;&gt;lift&lt;/span&gt;.&lt;span class=&quot;l-property&quot;&gt;targets&lt;/span&gt;) &lt;span class=&quot;l-operator&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;l-variable&quot;&gt;lift&lt;/span&gt;.&lt;span class=&quot;l-property&quot;&gt;floor&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;10&quot;&gt;            &lt;span class=&quot;l-keyword&quot;&gt;else&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;&amp;quot;down&amp;quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;11&quot;&gt;        )&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;12&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;13&quot;&gt;    &lt;span class=&quot;l-keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;l-variable&quot;&gt;lift&lt;/span&gt;.&lt;span class=&quot;l-property&quot;&gt;direction&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;&amp;quot;up&amp;quot;&lt;/span&gt;:&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;14&quot;&gt;        &lt;span class=&quot;l-variable&quot;&gt;above&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;=&lt;/span&gt; [&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;15&quot;&gt;            &lt;span class=&quot;l-variable&quot;&gt;floor&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;16&quot;&gt;            &lt;span class=&quot;l-keyword&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;l-variable&quot;&gt;floor&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;l-variable&quot;&gt;lift&lt;/span&gt;.&lt;span class=&quot;l-property&quot;&gt;targets&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;17&quot;&gt;            &lt;span class=&quot;l-keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;l-variable&quot;&gt;floor&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;l-variable&quot;&gt;lift&lt;/span&gt;.&lt;span class=&quot;l-property&quot;&gt;floor&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;18&quot;&gt;        ]&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;19&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;20&quot;&gt;        &lt;span class=&quot;l-keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;l-variable&quot;&gt;above&lt;/span&gt;:&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;21&quot;&gt;            &lt;span class=&quot;l-keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;l-function-builtin&quot;&gt;min&lt;/span&gt;(&lt;span class=&quot;l-variable&quot;&gt;above&lt;/span&gt;)&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;22&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;23&quot;&gt;        &lt;span class=&quot;l-variable&quot;&gt;lift&lt;/span&gt;.&lt;span class=&quot;l-property&quot;&gt;direction&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;&amp;quot;down&amp;quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;24&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;25&quot;&gt;    &lt;span class=&quot;l-keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;l-variable&quot;&gt;lift&lt;/span&gt;.&lt;span class=&quot;l-property&quot;&gt;direction&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;&amp;quot;down&amp;quot;&lt;/span&gt;:&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;26&quot;&gt;        &lt;span class=&quot;l-variable&quot;&gt;below&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;=&lt;/span&gt; [&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;27&quot;&gt;            &lt;span class=&quot;l-variable&quot;&gt;floor&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;28&quot;&gt;            &lt;span class=&quot;l-keyword&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;l-variable&quot;&gt;floor&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;l-variable&quot;&gt;lift&lt;/span&gt;.&lt;span class=&quot;l-property&quot;&gt;targets&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;29&quot;&gt;            &lt;span class=&quot;l-keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;l-variable&quot;&gt;floor&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;l-variable&quot;&gt;lift&lt;/span&gt;.&lt;span class=&quot;l-property&quot;&gt;floor&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;30&quot;&gt;        ]&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;31&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;32&quot;&gt;        &lt;span class=&quot;l-keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;l-variable&quot;&gt;below&lt;/span&gt;:&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;33&quot;&gt;            &lt;span class=&quot;l-keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;l-function-builtin&quot;&gt;max&lt;/span&gt;(&lt;span class=&quot;l-variable&quot;&gt;below&lt;/span&gt;)&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;34&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;35&quot;&gt;        &lt;span class=&quot;l-variable&quot;&gt;lift&lt;/span&gt;.&lt;span class=&quot;l-property&quot;&gt;direction&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;&amp;quot;up&amp;quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;36&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;37&quot;&gt;    &lt;span class=&quot;l-comment&quot;&gt;# If we get here, there are targets but none in the&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;38&quot;&gt;    &lt;span class=&quot;l-comment&quot;&gt;# current direction. Pick the closest one and establish&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;39&quot;&gt;    &lt;span class=&quot;l-comment&quot;&gt;# the corresponding direction.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;40&quot;&gt;    &lt;span class=&quot;l-variable&quot;&gt;target&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;l-function-builtin&quot;&gt;min&lt;/span&gt;(&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;41&quot;&gt;        &lt;span class=&quot;l-variable&quot;&gt;lift&lt;/span&gt;.&lt;span class=&quot;l-property&quot;&gt;targets&lt;/span&gt;,&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;42&quot;&gt;        &lt;span class=&quot;l-variable&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;l-keyword&quot;&gt;lambda&lt;/span&gt; &lt;span class=&quot;l-variable&quot;&gt;floor&lt;/span&gt;: &lt;span class=&quot;l-function-builtin&quot;&gt;abs&lt;/span&gt;(&lt;span class=&quot;l-variable&quot;&gt;floor&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;l-variable&quot;&gt;lift&lt;/span&gt;.&lt;span class=&quot;l-property&quot;&gt;floor&lt;/span&gt;),&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;43&quot;&gt;    )&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;44&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;45&quot;&gt;    &lt;span class=&quot;l-variable&quot;&gt;lift&lt;/span&gt;.&lt;span class=&quot;l-property&quot;&gt;direction&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;=&lt;/span&gt; (&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;46&quot;&gt;        &lt;span class=&quot;l-string&quot;&gt;&amp;quot;up&amp;quot;&lt;/span&gt; &lt;span class=&quot;l-keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;l-variable&quot;&gt;target&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;l-variable&quot;&gt;lift&lt;/span&gt;.&lt;span class=&quot;l-property&quot;&gt;floor&lt;/span&gt; &lt;span class=&quot;l-keyword&quot;&gt;else&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;&amp;quot;down&amp;quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;47&quot;&gt;    )&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;48&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;49&quot;&gt;    &lt;span class=&quot;l-keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;l-variable&quot;&gt;target&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For example:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-plaintext&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;span class=&quot;l-line&quot; data-line=&quot;1&quot;&gt;Lift:       5&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;2&quot;&gt;Requests:   2, 4, 6, 8, 10&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;3&quot;&gt;Direction:  up&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The lift visits:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-plaintext&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;span class=&quot;l-line&quot; data-line=&quot;1&quot;&gt;5 → 6 → 8 → 10 → 8 → 6 → 4 → 2&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;However, that example exposes an important implementation detail: &lt;strong&gt;once a target has been served, it must be removed from the target set&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;A simple simulation might therefore look like this:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-python&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;span class=&quot;l-line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;l-keyword&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;l-function&quot;&gt;serve_next&lt;/span&gt;(&lt;span class=&quot;l-variable&quot;&gt;lift&lt;/span&gt;: &lt;span class=&quot;l-type&quot;&gt;Lift&lt;/span&gt;) &lt;span class=&quot;l-operator&quot;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&quot;l-constant-builtin&quot;&gt;None&lt;/span&gt;:&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;2&quot;&gt;    &lt;span class=&quot;l-variable&quot;&gt;target&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;l-function&quot;&gt;next_target&lt;/span&gt;(&lt;span class=&quot;l-variable&quot;&gt;lift&lt;/span&gt;)&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;3&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;4&quot;&gt;    &lt;span class=&quot;l-keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;l-variable&quot;&gt;target&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;is&lt;/span&gt; &lt;span class=&quot;l-constant-builtin&quot;&gt;None&lt;/span&gt;:&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;5&quot;&gt;        &lt;span class=&quot;l-keyword&quot;&gt;return&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;6&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;7&quot;&gt;    &lt;span class=&quot;l-variable&quot;&gt;lift&lt;/span&gt;.&lt;span class=&quot;l-property&quot;&gt;floor&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;l-variable&quot;&gt;target&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;8&quot;&gt;    &lt;span class=&quot;l-variable&quot;&gt;lift&lt;/span&gt;.&lt;span class=&quot;l-property&quot;&gt;targets&lt;/span&gt;.&lt;span class=&quot;l-property&quot;&gt;remove&lt;/span&gt;(&lt;span class=&quot;l-variable&quot;&gt;target&lt;/span&gt;)&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now the route is correctly:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-plaintext&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;span class=&quot;l-line&quot; data-line=&quot;1&quot;&gt;5 → 6 → 8 → 10 → 4 → 2&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The lift continues upwards until there are no targets above it, then reverses direction.&lt;/p&gt;
&lt;h2&gt;A simpler implementation&lt;/h2&gt;
&lt;p&gt;We can make the LOOK behaviour more explicit by separating target selection from state changes:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-python&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;span class=&quot;l-line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;l-keyword&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;l-function&quot;&gt;next_target&lt;/span&gt;(&lt;span class=&quot;l-variable&quot;&gt;lift&lt;/span&gt;: &lt;span class=&quot;l-type&quot;&gt;Lift&lt;/span&gt;) &lt;span class=&quot;l-operator&quot;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&quot;l-variable&quot;&gt;int&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;l-constant-builtin&quot;&gt;None&lt;/span&gt;:&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;2&quot;&gt;    &lt;span class=&quot;l-keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;not&lt;/span&gt; &lt;span class=&quot;l-variable&quot;&gt;lift&lt;/span&gt;.&lt;span class=&quot;l-property&quot;&gt;targets&lt;/span&gt;:&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;3&quot;&gt;        &lt;span class=&quot;l-keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;l-constant-builtin&quot;&gt;None&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;4&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;5&quot;&gt;    &lt;span class=&quot;l-keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;l-variable&quot;&gt;lift&lt;/span&gt;.&lt;span class=&quot;l-property&quot;&gt;direction&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;&amp;quot;up&amp;quot;&lt;/span&gt;:&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;6&quot;&gt;        &lt;span class=&quot;l-variable&quot;&gt;above&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;l-function-builtin&quot;&gt;sorted&lt;/span&gt;(&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;7&quot;&gt;            &lt;span class=&quot;l-variable&quot;&gt;floor&lt;/span&gt; &lt;span class=&quot;l-keyword&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;l-variable&quot;&gt;floor&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;l-variable&quot;&gt;lift&lt;/span&gt;.&lt;span class=&quot;l-property&quot;&gt;targets&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;8&quot;&gt;            &lt;span class=&quot;l-keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;l-variable&quot;&gt;floor&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;l-variable&quot;&gt;lift&lt;/span&gt;.&lt;span class=&quot;l-property&quot;&gt;floor&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;9&quot;&gt;        )&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;10&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;11&quot;&gt;        &lt;span class=&quot;l-keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;l-variable&quot;&gt;above&lt;/span&gt;:&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;12&quot;&gt;            &lt;span class=&quot;l-keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;l-variable&quot;&gt;above&lt;/span&gt;[&lt;span class=&quot;l-number&quot;&gt;0&lt;/span&gt;]&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;13&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;14&quot;&gt;        &lt;span class=&quot;l-variable&quot;&gt;lift&lt;/span&gt;.&lt;span class=&quot;l-property&quot;&gt;direction&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;&amp;quot;down&amp;quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;15&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;16&quot;&gt;    &lt;span class=&quot;l-keyword&quot;&gt;elif&lt;/span&gt; &lt;span class=&quot;l-variable&quot;&gt;lift&lt;/span&gt;.&lt;span class=&quot;l-property&quot;&gt;direction&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;&amp;quot;down&amp;quot;&lt;/span&gt;:&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;17&quot;&gt;        &lt;span class=&quot;l-variable&quot;&gt;below&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;l-function-builtin&quot;&gt;sorted&lt;/span&gt;(&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;18&quot;&gt;            &lt;span class=&quot;l-variable&quot;&gt;floor&lt;/span&gt; &lt;span class=&quot;l-keyword&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;l-variable&quot;&gt;floor&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;l-variable&quot;&gt;lift&lt;/span&gt;.&lt;span class=&quot;l-property&quot;&gt;targets&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;19&quot;&gt;            &lt;span class=&quot;l-keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;l-variable&quot;&gt;floor&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;l-variable&quot;&gt;lift&lt;/span&gt;.&lt;span class=&quot;l-property&quot;&gt;floor&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;20&quot;&gt;        )&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;21&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;22&quot;&gt;        &lt;span class=&quot;l-keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;l-variable&quot;&gt;below&lt;/span&gt;:&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;23&quot;&gt;            &lt;span class=&quot;l-keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;l-variable&quot;&gt;below&lt;/span&gt;[&lt;span class=&quot;l-operator&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;l-number&quot;&gt;1&lt;/span&gt;]&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;24&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;25&quot;&gt;        &lt;span class=&quot;l-variable&quot;&gt;lift&lt;/span&gt;.&lt;span class=&quot;l-property&quot;&gt;direction&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;&amp;quot;up&amp;quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;26&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;27&quot;&gt;    &lt;span class=&quot;l-comment&quot;&gt;# The lift was idle, or has just changed direction.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;28&quot;&gt;    &lt;span class=&quot;l-keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;l-variable&quot;&gt;lift&lt;/span&gt;.&lt;span class=&quot;l-property&quot;&gt;direction&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;&amp;quot;up&amp;quot;&lt;/span&gt;:&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;29&quot;&gt;        &lt;span class=&quot;l-variable&quot;&gt;above&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;=&lt;/span&gt; [&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;30&quot;&gt;            &lt;span class=&quot;l-variable&quot;&gt;floor&lt;/span&gt; &lt;span class=&quot;l-keyword&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;l-variable&quot;&gt;floor&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;l-variable&quot;&gt;lift&lt;/span&gt;.&lt;span class=&quot;l-property&quot;&gt;targets&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;31&quot;&gt;            &lt;span class=&quot;l-keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;l-variable&quot;&gt;floor&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;l-variable&quot;&gt;lift&lt;/span&gt;.&lt;span class=&quot;l-property&quot;&gt;floor&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;32&quot;&gt;        ]&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;33&quot;&gt;        &lt;span class=&quot;l-keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;l-variable&quot;&gt;above&lt;/span&gt;:&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;34&quot;&gt;            &lt;span class=&quot;l-keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;l-function-builtin&quot;&gt;min&lt;/span&gt;(&lt;span class=&quot;l-variable&quot;&gt;above&lt;/span&gt;)&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;35&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;36&quot;&gt;    &lt;span class=&quot;l-keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;l-variable&quot;&gt;lift&lt;/span&gt;.&lt;span class=&quot;l-property&quot;&gt;direction&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;&amp;quot;down&amp;quot;&lt;/span&gt;:&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;37&quot;&gt;        &lt;span class=&quot;l-variable&quot;&gt;below&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;=&lt;/span&gt; [&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;38&quot;&gt;            &lt;span class=&quot;l-variable&quot;&gt;floor&lt;/span&gt; &lt;span class=&quot;l-keyword&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;l-variable&quot;&gt;floor&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;l-variable&quot;&gt;lift&lt;/span&gt;.&lt;span class=&quot;l-property&quot;&gt;targets&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;39&quot;&gt;            &lt;span class=&quot;l-keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;l-variable&quot;&gt;floor&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;l-variable&quot;&gt;lift&lt;/span&gt;.&lt;span class=&quot;l-property&quot;&gt;floor&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;40&quot;&gt;        ]&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;41&quot;&gt;        &lt;span class=&quot;l-keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;l-variable&quot;&gt;below&lt;/span&gt;:&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;42&quot;&gt;            &lt;span class=&quot;l-keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;l-function-builtin&quot;&gt;max&lt;/span&gt;(&lt;span class=&quot;l-variable&quot;&gt;below&lt;/span&gt;)&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;43&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;44&quot;&gt;    &lt;span class=&quot;l-keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;l-constant-builtin&quot;&gt;None&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The important distinction from a naive implementation is that LOOK &lt;strong&gt;does not choose the globally closest floor&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Given:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-plaintext&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;span class=&quot;l-line&quot; data-line=&quot;1&quot;&gt;Current floor: 5&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;2&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;3&quot;&gt;Targets:&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;4&quot;&gt;4&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;5&quot;&gt;6&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;6&quot;&gt;10&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;and direction &lt;code&gt;up&lt;/code&gt;, the next target is:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-plaintext&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;span class=&quot;l-line&quot; data-line=&quot;1&quot;&gt;6&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;not:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-plaintext&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;span class=&quot;l-line&quot; data-line=&quot;1&quot;&gt;4&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Even though both are equally close, the lift is already travelling upwards.&lt;/p&gt;
&lt;p&gt;After serving 6 and 10, it reverses:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-plaintext&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;span class=&quot;l-line&quot; data-line=&quot;1&quot;&gt;5 → 6 → 10 → 4&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That's the defining behaviour of LOOK.&lt;/p&gt;
&lt;h2&gt;Hall requests are different&lt;/h2&gt;
&lt;p&gt;There are actually two kinds of requests.&lt;/p&gt;
&lt;p&gt;A passenger inside the lift says:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-plaintext&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;span class=&quot;l-line&quot; data-line=&quot;1&quot;&gt;I want to go to floor 10.&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A passenger outside says:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-plaintext&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;span class=&quot;l-line&quot; data-line=&quot;1&quot;&gt;I&amp;#39;m on floor 5 and want to go up.&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The second request also has a direction.&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-python&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;span class=&quot;l-line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;l-function&quot;&gt;@&lt;span class=&quot;l-function&quot;&gt;dataclass&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;2&quot;&gt;&lt;span class=&quot;l-keyword&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;l-constructor&quot;&gt;HallRequest&lt;/span&gt;:&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;3&quot;&gt;    &lt;span class=&quot;l-variable&quot;&gt;floor&lt;/span&gt;: &lt;span class=&quot;l-type&quot;&gt;int&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;4&quot;&gt;    &lt;span class=&quot;l-variable&quot;&gt;direction&lt;/span&gt;: &lt;span class=&quot;l-type&quot;&gt;str&lt;/span&gt;  &lt;span class=&quot;l-comment&quot;&gt;# &amp;quot;up&amp;quot; or &amp;quot;down&amp;quot;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This matters because a lift travelling upwards shouldn't necessarily stop for someone waiting to travel downwards.&lt;/p&gt;
&lt;p&gt;For example, if the lift is travelling:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-plaintext&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;span class=&quot;l-line&quot; data-line=&quot;1&quot;&gt;2 → 5 → 8 → 10&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;a request on floor 4 for a downward journey doesn't necessarily belong on that route.&lt;/p&gt;
&lt;p&gt;The controller should generally prioritise requests matching its current direction.&lt;/p&gt;
&lt;h1&gt;Multiple lifts&lt;/h1&gt;
&lt;p&gt;Now the problem gets more interesting.&lt;/p&gt;
&lt;p&gt;Imagine three lifts:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-plaintext&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;span class=&quot;l-line&quot; data-line=&quot;1&quot;&gt;Lift A: floor 2, going up&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;2&quot;&gt;Lift B: floor 8, idle&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;3&quot;&gt;Lift C: floor 15, going down&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Someone on floor 6 presses the up button.&lt;/p&gt;
&lt;p&gt;The obvious solution is to choose the closest lift. But that's not necessarily optimal.&lt;/p&gt;
&lt;p&gt;Instead, we can give every lift a score.&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-python&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;span class=&quot;l-line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;l-keyword&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;l-function&quot;&gt;score&lt;/span&gt;(&lt;span class=&quot;l-variable&quot;&gt;lift&lt;/span&gt;: &lt;span class=&quot;l-type&quot;&gt;Lift&lt;/span&gt;, &lt;span class=&quot;l-variable&quot;&gt;request&lt;/span&gt;: &lt;span class=&quot;l-type&quot;&gt;HallRequest&lt;/span&gt;) &lt;span class=&quot;l-operator&quot;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&quot;l-type&quot;&gt;float&lt;/span&gt;:&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;2&quot;&gt;    &lt;span class=&quot;l-variable&quot;&gt;distance&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;l-function-builtin&quot;&gt;abs&lt;/span&gt;(&lt;span class=&quot;l-variable&quot;&gt;lift&lt;/span&gt;.&lt;span class=&quot;l-property&quot;&gt;floor&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;l-variable&quot;&gt;request&lt;/span&gt;.&lt;span class=&quot;l-property&quot;&gt;floor&lt;/span&gt;)&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;3&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;4&quot;&gt;    &lt;span class=&quot;l-keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;l-variable&quot;&gt;lift&lt;/span&gt;.&lt;span class=&quot;l-property&quot;&gt;direction&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;l-variable&quot;&gt;request&lt;/span&gt;.&lt;span class=&quot;l-property&quot;&gt;direction&lt;/span&gt;:&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;5&quot;&gt;        &lt;span class=&quot;l-variable&quot;&gt;direction_penalty&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;l-number&quot;&gt;0&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;6&quot;&gt;    &lt;span class=&quot;l-keyword&quot;&gt;elif&lt;/span&gt; &lt;span class=&quot;l-variable&quot;&gt;lift&lt;/span&gt;.&lt;span class=&quot;l-property&quot;&gt;direction&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;is&lt;/span&gt; &lt;span class=&quot;l-constant-builtin&quot;&gt;None&lt;/span&gt;:&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;7&quot;&gt;        &lt;span class=&quot;l-variable&quot;&gt;direction_penalty&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;l-number&quot;&gt;2&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;8&quot;&gt;    &lt;span class=&quot;l-keyword&quot;&gt;else&lt;/span&gt;:&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;9&quot;&gt;        &lt;span class=&quot;l-variable&quot;&gt;direction_penalty&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;l-number&quot;&gt;10&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;10&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;11&quot;&gt;    &lt;span class=&quot;l-keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;l-variable&quot;&gt;distance&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;l-variable&quot;&gt;direction_penalty&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We can then select the lowest-scoring lift:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-python&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;span class=&quot;l-line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;l-keyword&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;l-function&quot;&gt;assign_lift&lt;/span&gt;(&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;2&quot;&gt;    &lt;span class=&quot;l-variable&quot;&gt;lifts&lt;/span&gt;: &lt;span class=&quot;l-variable&quot;&gt;list&lt;/span&gt;[&lt;span class=&quot;l-type&quot;&gt;Lift&lt;/span&gt;],&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;3&quot;&gt;    &lt;span class=&quot;l-variable&quot;&gt;request&lt;/span&gt;: &lt;span class=&quot;l-type&quot;&gt;HallRequest&lt;/span&gt;,&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;4&quot;&gt;) &lt;span class=&quot;l-operator&quot;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&quot;l-type&quot;&gt;Lift&lt;/span&gt;:&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;5&quot;&gt;    &lt;span class=&quot;l-keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;l-function-builtin&quot;&gt;min&lt;/span&gt;(&lt;span class=&quot;l-variable&quot;&gt;lifts&lt;/span&gt;, &lt;span class=&quot;l-variable&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;l-keyword&quot;&gt;lambda&lt;/span&gt; &lt;span class=&quot;l-variable&quot;&gt;lift&lt;/span&gt;: &lt;span class=&quot;l-function&quot;&gt;score&lt;/span&gt;(&lt;span class=&quot;l-variable&quot;&gt;lift&lt;/span&gt;, &lt;span class=&quot;l-variable&quot;&gt;request&lt;/span&gt;))&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is already considerably better than simply selecting the nearest lift.&lt;/p&gt;
&lt;h1&gt;Simulating the cost&lt;/h1&gt;
&lt;p&gt;A more sophisticated controller doesn't just ask:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;How far away is this lift?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;It asks:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;How much additional work will this request cause?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Consider a lift currently at floor 3 with these destinations:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-plaintext&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;span class=&quot;l-line&quot; data-line=&quot;1&quot;&gt;7, 10, 15&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A new passenger wants to go from floor 5 to floor 12.&lt;/p&gt;
&lt;p&gt;The existing route might be:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-plaintext&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;span class=&quot;l-line&quot; data-line=&quot;1&quot;&gt;3 → 7 → 10 → 15&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Adding the passenger changes it to:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-plaintext&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;span class=&quot;l-line&quot; data-line=&quot;1&quot;&gt;3 → 5 → 7 → 10 → 12 → 15&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The difference between those routes is the &lt;strong&gt;cost of accepting the request&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;We can model this with a cost function:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-python&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;span class=&quot;l-line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;l-keyword&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;l-function&quot;&gt;request_cost&lt;/span&gt;(&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;2&quot;&gt;    &lt;span class=&quot;l-variable&quot;&gt;waiting_time&lt;/span&gt;: &lt;span class=&quot;l-type&quot;&gt;float&lt;/span&gt;,&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;3&quot;&gt;    &lt;span class=&quot;l-variable&quot;&gt;travel_time&lt;/span&gt;: &lt;span class=&quot;l-type&quot;&gt;float&lt;/span&gt;,&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;4&quot;&gt;    &lt;span class=&quot;l-variable&quot;&gt;additional_stops&lt;/span&gt;: &lt;span class=&quot;l-type&quot;&gt;int&lt;/span&gt;,&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;5&quot;&gt;    &lt;span class=&quot;l-variable&quot;&gt;direction_changes&lt;/span&gt;: &lt;span class=&quot;l-type&quot;&gt;int&lt;/span&gt;,&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;6&quot;&gt;) &lt;span class=&quot;l-operator&quot;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&quot;l-type&quot;&gt;float&lt;/span&gt;:&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;7&quot;&gt;    &lt;span class=&quot;l-keyword&quot;&gt;return&lt;/span&gt; (&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;8&quot;&gt;        &lt;span class=&quot;l-variable&quot;&gt;waiting_time&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;l-number&quot;&gt;5&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;9&quot;&gt;        &lt;span class=&quot;l-operator&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;l-variable&quot;&gt;travel_time&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;10&quot;&gt;        &lt;span class=&quot;l-operator&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;l-variable&quot;&gt;additional_stops&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;l-number&quot;&gt;2&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;11&quot;&gt;        &lt;span class=&quot;l-operator&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;l-variable&quot;&gt;direction_changes&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;l-number&quot;&gt;10&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;12&quot;&gt;    )&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The weights are deliberately configurable.&lt;/p&gt;
&lt;p&gt;For example, we might consider waiting five times more important than travel time because making somebody wait for a lift is generally worse than adding a few seconds to the journey of someone already inside.&lt;/p&gt;
&lt;h1&gt;Destination dispatch&lt;/h1&gt;
&lt;p&gt;Modern lift systems can go one step further.&lt;/p&gt;
&lt;p&gt;Instead of pressing:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-plaintext&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;span class=&quot;l-line&quot; data-line=&quot;1&quot;&gt;↑&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;and later selecting a destination inside the lift, the passenger enters the destination immediately:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-plaintext&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;span class=&quot;l-line&quot; data-line=&quot;1&quot;&gt;Floor 5 → Floor 17&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The controller therefore knows the complete journey before assigning a lift.&lt;/p&gt;
&lt;p&gt;That allows it to group passengers with similar destinations.&lt;/p&gt;
&lt;p&gt;For example:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-plaintext&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;span class=&quot;l-line&quot; data-line=&quot;1&quot;&gt;3 → 17&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;2&quot;&gt;4 → 18&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;3&quot;&gt;5 → 16&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;4&quot;&gt;6 → 17&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;could potentially be handled by the same lift.&lt;/p&gt;
&lt;p&gt;The dispatcher can therefore optimise the entire route instead of treating every passenger as an independent request.&lt;/p&gt;
&lt;h2&gt;Separating the dispatcher from the lift&lt;/h2&gt;
&lt;p&gt;A useful architecture is to separate two responsibilities.&lt;/p&gt;
&lt;p&gt;The &lt;strong&gt;dispatcher&lt;/strong&gt; decides:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-plaintext&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;span class=&quot;l-line&quot; data-line=&quot;1&quot;&gt;Which lift should handle this request?&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;strong&gt;lift controller&lt;/strong&gt; decides:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-plaintext&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;span class=&quot;l-line&quot; data-line=&quot;1&quot;&gt;Which floor should I visit next?&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In Python, that could look like:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-python&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;span class=&quot;l-line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;l-keyword&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;l-constructor&quot;&gt;Dispatcher&lt;/span&gt;:&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;2&quot;&gt;    &lt;span class=&quot;l-keyword&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;l-function&quot;&gt;__init__&lt;/span&gt;(&lt;span class=&quot;l-variable&quot;&gt;self&lt;/span&gt;, &lt;span class=&quot;l-variable&quot;&gt;lifts&lt;/span&gt;: &lt;span class=&quot;l-variable&quot;&gt;list&lt;/span&gt;[&lt;span class=&quot;l-type&quot;&gt;Lift&lt;/span&gt;]):&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;3&quot;&gt;        &lt;span class=&quot;l-variable&quot;&gt;self&lt;/span&gt;.&lt;span class=&quot;l-property&quot;&gt;lifts&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;l-variable&quot;&gt;lifts&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;4&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;5&quot;&gt;    &lt;span class=&quot;l-keyword&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;l-function&quot;&gt;assign&lt;/span&gt;(&lt;span class=&quot;l-variable&quot;&gt;self&lt;/span&gt;, &lt;span class=&quot;l-variable&quot;&gt;request&lt;/span&gt;: &lt;span class=&quot;l-type&quot;&gt;HallRequest&lt;/span&gt;) &lt;span class=&quot;l-operator&quot;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&quot;l-type&quot;&gt;Lift&lt;/span&gt;:&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;6&quot;&gt;        &lt;span class=&quot;l-keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;l-function-builtin&quot;&gt;min&lt;/span&gt;(&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;7&quot;&gt;            &lt;span class=&quot;l-variable&quot;&gt;self&lt;/span&gt;.&lt;span class=&quot;l-property&quot;&gt;lifts&lt;/span&gt;,&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;8&quot;&gt;            &lt;span class=&quot;l-variable&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;l-keyword&quot;&gt;lambda&lt;/span&gt; &lt;span class=&quot;l-variable&quot;&gt;lift&lt;/span&gt;: &lt;span class=&quot;l-function&quot;&gt;score&lt;/span&gt;(&lt;span class=&quot;l-variable&quot;&gt;lift&lt;/span&gt;, &lt;span class=&quot;l-variable&quot;&gt;request&lt;/span&gt;),&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;9&quot;&gt;        )&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The lift itself can remain responsible for its own state:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-python&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;span class=&quot;l-line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;l-keyword&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;l-constructor&quot;&gt;LiftController&lt;/span&gt;:&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;2&quot;&gt;    &lt;span class=&quot;l-keyword&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;l-function&quot;&gt;__init__&lt;/span&gt;(&lt;span class=&quot;l-variable&quot;&gt;self&lt;/span&gt;, &lt;span class=&quot;l-variable&quot;&gt;lift&lt;/span&gt;: &lt;span class=&quot;l-type&quot;&gt;Lift&lt;/span&gt;):&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;3&quot;&gt;        &lt;span class=&quot;l-variable&quot;&gt;self&lt;/span&gt;.&lt;span class=&quot;l-property&quot;&gt;lift&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;l-variable&quot;&gt;lift&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;4&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;5&quot;&gt;    &lt;span class=&quot;l-keyword&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;l-function&quot;&gt;add_target&lt;/span&gt;(&lt;span class=&quot;l-variable&quot;&gt;self&lt;/span&gt;, &lt;span class=&quot;l-variable&quot;&gt;floor&lt;/span&gt;: &lt;span class=&quot;l-type&quot;&gt;int&lt;/span&gt;) &lt;span class=&quot;l-operator&quot;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&quot;l-constant-builtin&quot;&gt;None&lt;/span&gt;:&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;6&quot;&gt;        &lt;span class=&quot;l-variable&quot;&gt;self&lt;/span&gt;.&lt;span class=&quot;l-property&quot;&gt;lift&lt;/span&gt;.&lt;span class=&quot;l-property&quot;&gt;targets&lt;/span&gt;.&lt;span class=&quot;l-property&quot;&gt;add&lt;/span&gt;(&lt;span class=&quot;l-variable&quot;&gt;floor&lt;/span&gt;)&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;7&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;8&quot;&gt;    &lt;span class=&quot;l-keyword&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;l-function&quot;&gt;next_floor&lt;/span&gt;(&lt;span class=&quot;l-variable&quot;&gt;self&lt;/span&gt;) &lt;span class=&quot;l-operator&quot;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&quot;l-variable&quot;&gt;int&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;l-constant-builtin&quot;&gt;None&lt;/span&gt;:&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;9&quot;&gt;        &lt;span class=&quot;l-keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;l-function&quot;&gt;next_target&lt;/span&gt;(&lt;span class=&quot;l-variable&quot;&gt;self&lt;/span&gt;.&lt;span class=&quot;l-property&quot;&gt;lift&lt;/span&gt;)&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This separation makes the system much easier to reason about and test.&lt;/p&gt;
&lt;h1&gt;Model it as a state machine&lt;/h1&gt;
&lt;p&gt;A real controller would also have explicit states:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-python&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;span class=&quot;l-line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;l-keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;l-variable&quot;&gt;enum&lt;/span&gt; &lt;span class=&quot;l-keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;l-constructor&quot;&gt;Enum&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;2&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;3&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;4&quot;&gt;&lt;span class=&quot;l-keyword&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;l-constructor&quot;&gt;LiftState&lt;/span&gt;(&lt;span class=&quot;l-constructor&quot;&gt;Enum&lt;/span&gt;):&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;5&quot;&gt;    &lt;span class=&quot;l-constant&quot;&gt;IDLE&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;&amp;quot;idle&amp;quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;6&quot;&gt;    &lt;span class=&quot;l-constant&quot;&gt;MOVING_UP&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;&amp;quot;moving_up&amp;quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;7&quot;&gt;    &lt;span class=&quot;l-constant&quot;&gt;MOVING_DOWN&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;&amp;quot;moving_down&amp;quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;8&quot;&gt;    &lt;span class=&quot;l-constant&quot;&gt;DOOR_OPENING&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;&amp;quot;door_opening&amp;quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;9&quot;&gt;    &lt;span class=&quot;l-constant&quot;&gt;DOOR_OPEN&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;&amp;quot;door_open&amp;quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;10&quot;&gt;    &lt;span class=&quot;l-constant&quot;&gt;DOOR_CLOSING&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;&amp;quot;door_closing&amp;quot;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Events then cause state transitions:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-plaintext&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;span class=&quot;l-line&quot; data-line=&quot;1&quot;&gt;IDLE&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;2&quot;&gt;  ↓ request above&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;3&quot;&gt;MOVING_UP&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;4&quot;&gt;  ↓ target reached&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;5&quot;&gt;DOOR_OPENING&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;6&quot;&gt;  ↓&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;7&quot;&gt;DOOR_OPEN&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;8&quot;&gt;  ↓&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;9&quot;&gt;DOOR_CLOSING&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;10&quot;&gt;  ↓&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;11&quot;&gt;MOVING_UP&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This becomes particularly useful once you need to handle things such as emergency stops, overloaded lifts, door obstruction, and new requests arriving while the lift is moving.&lt;/p&gt;
&lt;h2&gt;The interesting part is the optimisation&lt;/h2&gt;
&lt;p&gt;The basic mechanics of a lift are relatively straightforward.&lt;/p&gt;
&lt;p&gt;The difficult part is deciding what &lt;strong&gt;optimal&lt;/strong&gt; means.&lt;/p&gt;
&lt;p&gt;A real controller might try to minimise:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-plaintext&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;span class=&quot;l-line&quot; data-line=&quot;1&quot;&gt;total waiting time&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;2&quot;&gt;+ total travel time&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;3&quot;&gt;+ number of stops&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;4&quot;&gt;+ direction changes&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;5&quot;&gt;+ energy consumption&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;6&quot;&gt;+ passenger congestion&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And these objectives can conflict.&lt;/p&gt;
&lt;p&gt;Optimising one passenger's journey may make everybody else's journey worse.&lt;/p&gt;
&lt;p&gt;That's why lift scheduling is a useful example of a broader class of software problems: &lt;strong&gt;resource allocation under changing constraints&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;The same pattern appears in job schedulers, CPU scheduling, delivery routing, database query planning, and many other systems.&lt;/p&gt;
&lt;p&gt;You don't necessarily need to find the mathematically perfect solution. A good practical approach is often:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Model the current state.&lt;/li&gt;
&lt;li&gt;Generate possible decisions.&lt;/li&gt;
&lt;li&gt;Estimate the cost of each decision.&lt;/li&gt;
&lt;li&gt;Pick the lowest-cost option.&lt;/li&gt;
&lt;li&gt;Recalculate when the state changes.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;For a lift, that's enough to go from a simple &amp;quot;nearest lift wins&amp;quot; implementation to a surprisingly sophisticated scheduling system.&lt;/p&gt;&lt;p&gt;&lt;a href=&quot;https://www.yellowduck.be/tags/development&quot;&gt;#development&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/pattern&quot;&gt;#pattern&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/python&quot;&gt;#python&lt;/a&gt;&lt;/p&gt;</content>
  </entry>
  <entry>
    <id>https://www.yellowduck.be/posts/cross-schema-queries-in-postgresql-without-the-loop</id>
    <title>🐥 Cross-schema queries in PostgreSQL without the loop</title>
    <updated>2026-10-08T17:00:00Z</updated>
    <published>2026-10-08T17:00:00Z</published>
    <author><name>Pieter Claerhout</name><email>pieter@yellowduck.be</email></author>
    <link rel="alternate" href="https://www.yellowduck.be/posts/cross-schema-queries-in-postgresql-without-the-loop"/>
    <content type="html">&lt;p&gt;Schema-based multi-tenancy is a popular pattern in PostgreSQL SaaS applications. Each tenant gets its own schema — &lt;code&gt;tenant_1&lt;/code&gt;, &lt;code&gt;tenant_2&lt;/code&gt;, and so on — with an identical table structure but fully isolated data. The application layer queries the right schema at runtime, and the database enforces the separation cleanly.&lt;/p&gt;
&lt;p&gt;The headache arrives when you need to query &lt;em&gt;across&lt;/em&gt; tenants: a one-off data audit, a cross-tenant report, a support investigation. The table exists in every schema, but there is no built-in &lt;code&gt;SELECT * FROM ALL SCHEMAS&lt;/code&gt; syntax. You have to build that query yourself.&lt;/p&gt;
&lt;h1&gt;The procedural approach&lt;/h1&gt;
&lt;p&gt;The first instinct is usually a loop. Iterate over every tenant schema, query the table in each one, and collect the results somewhere. In PL/pgSQL:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-sql&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;span class=&quot;l-line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;l-comment&quot;&gt;-- Step 1: Create a staging table using the first tenant&amp;#39;s column structure&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;2&quot;&gt;DO $$&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;3&quot;&gt;&lt;span class=&quot;l-keyword&quot;&gt;DECLARE&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;4&quot;&gt;    sch &lt;span class=&quot;l-type-builtin&quot;&gt;text&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;5&quot;&gt;&lt;span class=&quot;l-keyword&quot;&gt;BEGIN&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;6&quot;&gt;    FOR sch &lt;span class=&quot;l-keyword-operator&quot;&gt;IN&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;7&quot;&gt;        &lt;span class=&quot;l-keyword&quot;&gt;SELECT&lt;/span&gt; &lt;span class=&quot;l-variable-member&quot;&gt;nspname&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;8&quot;&gt;          &lt;span class=&quot;l-keyword&quot;&gt;FROM&lt;/span&gt; &lt;span class=&quot;l-type&quot;&gt;pg_namespace&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;9&quot;&gt;         &lt;span class=&quot;l-keyword&quot;&gt;WHERE&lt;/span&gt; &lt;span class=&quot;l-variable-member&quot;&gt;nspname&lt;/span&gt; &lt;span class=&quot;l-keyword&quot;&gt;LIKE&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;&amp;#39;tenant\_%&amp;#39;&lt;/span&gt; ESCAPE &amp;#39;\&amp;#39;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;10&quot;&gt;         &lt;span class=&quot;l-keyword&quot;&gt;ORDER&lt;/span&gt; &lt;span class=&quot;l-keyword-operator&quot;&gt;BY&lt;/span&gt; &lt;span class=&quot;l-variable-member&quot;&gt;nspname&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;11&quot;&gt;         &lt;span class=&quot;l-keyword&quot;&gt;LIMIT&lt;/span&gt; &lt;span class=&quot;l-number&quot;&gt;1&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;12&quot;&gt;    LOOP&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;13&quot;&gt;        &lt;span class=&quot;l-keyword&quot;&gt;EXECUTE&lt;/span&gt; &lt;span class=&quot;l-keyword&quot;&gt;format&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;14&quot;&gt;            &amp;#39;CREATE TEMP TABLE all_records AS&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;15&quot;&gt;             SELECT %L::text AS tenant, * FROM %I.orders WHERE false&amp;#39;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;16&quot;&gt;            sch&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; sch&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;17&quot;&gt;        &lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;18&quot;&gt;    &lt;span class=&quot;l-keyword&quot;&gt;END&lt;/span&gt; LOOP&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;19&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;20&quot;&gt;    &lt;span class=&quot;l-comment&quot;&gt;-- Step 2: Populate from every tenant schema&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;21&quot;&gt;    FOR sch &lt;span class=&quot;l-keyword-operator&quot;&gt;IN&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;22&quot;&gt;        &lt;span class=&quot;l-keyword&quot;&gt;SELECT&lt;/span&gt; &lt;span class=&quot;l-variable-member&quot;&gt;nspname&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;23&quot;&gt;          &lt;span class=&quot;l-keyword&quot;&gt;FROM&lt;/span&gt; &lt;span class=&quot;l-type&quot;&gt;pg_namespace&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;24&quot;&gt;         &lt;span class=&quot;l-keyword&quot;&gt;WHERE&lt;/span&gt; &lt;span class=&quot;l-variable-member&quot;&gt;nspname&lt;/span&gt; &lt;span class=&quot;l-keyword&quot;&gt;LIKE&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;&amp;#39;tenant\_%&amp;#39;&lt;/span&gt; ESCAPE &amp;#39;\&amp;#39;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;25&quot;&gt;         &lt;span class=&quot;l-keyword&quot;&gt;ORDER&lt;/span&gt; &lt;span class=&quot;l-keyword-operator&quot;&gt;BY&lt;/span&gt; &lt;span class=&quot;l-type&quot;&gt;nspname&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;26&quot;&gt;    LOOP&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;27&quot;&gt;        &lt;span class=&quot;l-keyword&quot;&gt;EXECUTE&lt;/span&gt; &lt;span class=&quot;l-keyword&quot;&gt;format&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;28&quot;&gt;            &lt;span class=&quot;l-string&quot;&gt;&amp;#39;INSERT INTO all_records&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;29&quot;&gt;&lt;span class=&quot;l-string&quot;&gt;             SELECT %L::text AS tenant, * FROM %I.orders&amp;#39;&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;30&quot;&gt;            &lt;span class=&quot;l-variable-member&quot;&gt;sch&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-variable-member&quot;&gt;sch&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;31&quot;&gt;        &lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;32&quot;&gt;    END LOOP&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;33&quot;&gt;&lt;span class=&quot;l-keyword&quot;&gt;END&lt;/span&gt; $$&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;34&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;35&quot;&gt;&lt;span class=&quot;l-keyword&quot;&gt;SELECT&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;l-keyword&quot;&gt;FROM&lt;/span&gt; &lt;span class=&quot;l-type&quot;&gt;all_records&lt;/span&gt; &lt;span class=&quot;l-keyword&quot;&gt;ORDER&lt;/span&gt; &lt;span class=&quot;l-keyword-operator&quot;&gt;BY&lt;/span&gt; &lt;span class=&quot;l-variable-member&quot;&gt;tenant&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;36&quot;&gt;&lt;span class=&quot;l-keyword&quot;&gt;DROP&lt;/span&gt; &lt;span class=&quot;l-keyword&quot;&gt;TABLE&lt;/span&gt; &lt;span class=&quot;l-type&quot;&gt;all_records&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This works. The first loop borrows a schema's column structure to create the temp table; the second loop populates it. But there is an awkward quality to it: two loops, two passes over &lt;code&gt;pg_namespace&lt;/code&gt;, and a &lt;code&gt;DO&lt;/code&gt; block that can't return rows directly, so a temp table is required as a staging area.&lt;/p&gt;
&lt;h1&gt;The set-based approach&lt;/h1&gt;
&lt;p&gt;PostgreSQL's &lt;code&gt;string_agg&lt;/code&gt; function lets you collapse the entire loop into a single expression. Instead of visiting schemas one at a time, you assemble the full &lt;code&gt;UNION ALL&lt;/code&gt; query string in one pass, then execute it once:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-sql&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;span class=&quot;l-line&quot; data-line=&quot;1&quot;&gt;DO $$&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;2&quot;&gt;&lt;span class=&quot;l-keyword&quot;&gt;DECLARE&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;3&quot;&gt;    sql &lt;span class=&quot;l-type-builtin&quot;&gt;text&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;4&quot;&gt;&lt;span class=&quot;l-keyword&quot;&gt;BEGIN&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;5&quot;&gt;    &lt;span class=&quot;l-keyword&quot;&gt;SELECT&lt;/span&gt; &lt;span class=&quot;l-type&quot;&gt;string_agg&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;6&quot;&gt;        &lt;span class=&quot;l-type&quot;&gt;format&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;7&quot;&gt;            &lt;span class=&quot;l-string&quot;&gt;&amp;#39;SELECT %L::text AS tenant, * FROM %I.orders&amp;#39;&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;8&quot;&gt;            &lt;span class=&quot;l-variable-member&quot;&gt;nspname&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-variable-member&quot;&gt;nspname&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;9&quot;&gt;        &lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;10&quot;&gt;        &lt;span class=&quot;l-string&quot;&gt;&amp;#39; UNION ALL &amp;#39;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;11&quot;&gt;        &lt;span class=&quot;l-keyword&quot;&gt;ORDER&lt;/span&gt; &lt;span class=&quot;l-keyword-operator&quot;&gt;BY&lt;/span&gt; &lt;span class=&quot;l-variable-member&quot;&gt;nspname&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;12&quot;&gt;    &lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;13&quot;&gt;    &lt;span class=&quot;l-keyword&quot;&gt;INTO&lt;/span&gt; &lt;span class=&quot;l-variable-member&quot;&gt;sql&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;14&quot;&gt;    &lt;span class=&quot;l-keyword&quot;&gt;FROM&lt;/span&gt; &lt;span class=&quot;l-type&quot;&gt;pg_namespace&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;15&quot;&gt;    &lt;span class=&quot;l-keyword&quot;&gt;WHERE&lt;/span&gt; &lt;span class=&quot;l-variable-member&quot;&gt;nspname&lt;/span&gt; &lt;span class=&quot;l-keyword&quot;&gt;LIKE&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;&amp;#39;tenant\_%&amp;#39;&lt;/span&gt; ESCAPE &amp;#39;\&amp;#39;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;16&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;17&quot;&gt;    EXECUTE &amp;#39;CREATE TEMP TABLE all_records AS &amp;#39; || sql&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;18&quot;&gt;&lt;span class=&quot;l-keyword&quot;&gt;END&lt;/span&gt; $$&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;19&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;20&quot;&gt;&lt;span class=&quot;l-keyword&quot;&gt;SELECT&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;l-keyword&quot;&gt;FROM&lt;/span&gt; &lt;span class=&quot;l-type&quot;&gt;all_records&lt;/span&gt; &lt;span class=&quot;l-keyword&quot;&gt;ORDER&lt;/span&gt; &lt;span class=&quot;l-keyword-operator&quot;&gt;BY&lt;/span&gt; &lt;span class=&quot;l-variable-member&quot;&gt;tenant&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;21&quot;&gt;&lt;span class=&quot;l-keyword&quot;&gt;DROP&lt;/span&gt; &lt;span class=&quot;l-keyword&quot;&gt;TABLE&lt;/span&gt; &lt;span class=&quot;l-type&quot;&gt;all_records&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;No loop. &lt;code&gt;string_agg&lt;/code&gt; scans &lt;code&gt;pg_namespace&lt;/code&gt; once and produces a string like:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-sql&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;span class=&quot;l-line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;l-keyword&quot;&gt;SELECT&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;&amp;#39;tenant_1&amp;#39;&lt;/span&gt;::&lt;span class=&quot;l-type-builtin&quot;&gt;text&lt;/span&gt; &lt;span class=&quot;l-keyword&quot;&gt;AS&lt;/span&gt; &lt;span class=&quot;l-variable&quot;&gt;tenant&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;l-keyword&quot;&gt;FROM&lt;/span&gt; tenant_1&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;l-type&quot;&gt;orders&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;2&quot;&gt;&lt;span class=&quot;l-keyword-operator&quot;&gt;UNION&lt;/span&gt; &lt;span class=&quot;l-keyword&quot;&gt;ALL&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;3&quot;&gt;&lt;span class=&quot;l-keyword&quot;&gt;SELECT&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;&amp;#39;tenant_2&amp;#39;&lt;/span&gt;::&lt;span class=&quot;l-type-builtin&quot;&gt;text&lt;/span&gt; &lt;span class=&quot;l-keyword&quot;&gt;AS&lt;/span&gt; &lt;span class=&quot;l-variable&quot;&gt;tenant&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;l-keyword&quot;&gt;FROM&lt;/span&gt; tenant_2&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;l-type&quot;&gt;orders&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;4&quot;&gt;&lt;span class=&quot;l-keyword-operator&quot;&gt;UNION&lt;/span&gt; &lt;span class=&quot;l-keyword&quot;&gt;ALL&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;5&quot;&gt;&lt;span class=&quot;l-keyword&quot;&gt;SELECT&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;&amp;#39;tenant_3&amp;#39;&lt;/span&gt;::&lt;span class=&quot;l-type-builtin&quot;&gt;text&lt;/span&gt; &lt;span class=&quot;l-keyword&quot;&gt;AS&lt;/span&gt; &lt;span class=&quot;l-variable&quot;&gt;tenant&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;l-keyword&quot;&gt;FROM&lt;/span&gt; tenant_3&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;l-type&quot;&gt;orders&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A single &lt;code&gt;EXECUTE&lt;/code&gt; then runs that string wrapped in a &lt;code&gt;CREATE TEMP TABLE AS&lt;/code&gt;, which also infers the column structure automatically — no need for a separate setup step.&lt;/p&gt;
&lt;h1&gt;Breaking It Down&lt;/h1&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Element&lt;/th&gt;
&lt;th&gt;What it does&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;pg_namespace&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;PostgreSQL's system catalog table for schemas. Filter by &lt;code&gt;nspname&lt;/code&gt; to target tenant schemas.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;format('%I', …)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Safely quotes an identifier (schema name, table name). Prevents SQL injection in dynamic queries.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;format('%L', …)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Safely quotes a string literal. Used here to emit the tenant name as a constant in each sub-query.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;string_agg(…, ' UNION ALL ')&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Joins all sub-queries into one SQL string. The &lt;code&gt;ORDER BY&lt;/code&gt; clause ensures a deterministic join order.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;CREATE TEMP TABLE AS …&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Creates a temporary table whose columns are inferred from the query result. Scoped to the session and dropped automatically when the connection closes.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;On format() and injection safety&lt;/strong&gt; — Always use &lt;code&gt;%I&lt;/code&gt; for identifiers and &lt;code&gt;%L&lt;/code&gt; for literals when building dynamic SQL with &lt;code&gt;format()&lt;/code&gt;. They handle quoting and escaping correctly, even for edge cases like schema names containing reserved words or special characters.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h1&gt;When to reach for this&lt;/h1&gt;
&lt;p&gt;This pattern is well suited to ad-hoc, operational queries:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Cross-tenant reporting and aggregations&lt;/li&gt;
&lt;li&gt;Data consistency audits across all schemas&lt;/li&gt;
&lt;li&gt;Support investigations that span multiple tenants&lt;/li&gt;
&lt;li&gt;One-off backfill or data migration checks&lt;/li&gt;
&lt;li&gt;Debugging anomalies that may appear in only some tenants&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For production cross-tenant queries that run on a schedule, a dedicated reporting schema or a materialized view strategy is usually a better fit. Dynamic SQL assembled at query time adds latency and bypasses the planner's ability to optimize across tenants as a whole.&lt;/p&gt;
&lt;h2&gt;The underlying idea&lt;/h2&gt;
&lt;p&gt;The loop version is imperative: it tells the database &lt;em&gt;how&lt;/em&gt; to visit each schema. The &lt;code&gt;string_agg&lt;/code&gt; version is declarative: it describes the result it wants and lets a single &lt;code&gt;EXECUTE&lt;/code&gt; handle execution. That shift is modest here, but it reflects a broader principle in SQL — set-based operations tend to be shorter, easier to read, and easier to extend than their procedural equivalents.&lt;/p&gt;
&lt;p&gt;Adding a filter is illustrative. In the loop version you modify the format string inside the loop body and track whether you've touched it everywhere. In the &lt;code&gt;string_agg&lt;/code&gt; version you change the format string in one place:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-sql&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;span class=&quot;l-line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;l-keyword&quot;&gt;SELECT&lt;/span&gt; &lt;span class=&quot;l-type&quot;&gt;string_agg&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;2&quot;&gt;    &lt;span class=&quot;l-type&quot;&gt;format&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;3&quot;&gt;        &lt;span class=&quot;l-string&quot;&gt;&amp;#39;SELECT %L::text AS tenant, * FROM %I.orders WHERE status = &amp;#39;&amp;#39;pending&amp;#39;&amp;#39;&amp;#39;&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;4&quot;&gt;        &lt;span class=&quot;l-variable-member&quot;&gt;nspname&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-variable-member&quot;&gt;nspname&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;5&quot;&gt;    &lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;6&quot;&gt;    &lt;span class=&quot;l-string&quot;&gt;&amp;#39; UNION ALL &amp;#39;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;7&quot;&gt;    &lt;span class=&quot;l-keyword&quot;&gt;ORDER&lt;/span&gt; &lt;span class=&quot;l-keyword-operator&quot;&gt;BY&lt;/span&gt; &lt;span class=&quot;l-variable-member&quot;&gt;nspname&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;8&quot;&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;9&quot;&gt;&lt;span class=&quot;l-keyword&quot;&gt;INTO&lt;/span&gt; &lt;span class=&quot;l-variable-member&quot;&gt;sql&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;10&quot;&gt;&lt;span class=&quot;l-keyword&quot;&gt;FROM&lt;/span&gt; &lt;span class=&quot;l-type&quot;&gt;pg_namespace&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;11&quot;&gt;&lt;span class=&quot;l-keyword&quot;&gt;WHERE&lt;/span&gt; &lt;span class=&quot;l-variable-member&quot;&gt;nspname&lt;/span&gt; &lt;span class=&quot;l-keyword&quot;&gt;LIKE&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;&amp;#39;tenant\_%&amp;#39;&lt;/span&gt; ESCAPE &amp;#39;\&amp;#39;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;One change, one place. The loop version requires the same change inside the loop body, with the same effect — but the set-based version makes the scope of the change obvious at a glance.&lt;/p&gt;&lt;p&gt;&lt;a href=&quot;https://www.yellowduck.be/tags/database&quot;&gt;#database&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/postgresql&quot;&gt;#postgresql&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/sql&quot;&gt;#sql&lt;/a&gt;&lt;/p&gt;</content>
  </entry>
  <entry>
    <id>https://www.yellowduck.be/posts/id-design-and-primary-keys-pt-1</id>
    <title>🔗 ID design and primary keys, pt. 1</title>
    <updated>2026-09-27T17:00:00Z</updated>
    <published>2026-09-27T17:00:00Z</published>
    <author><name>Pieter Claerhout</name><email>pieter@yellowduck.be</email></author>
    <link rel="alternate" href="https://www.yellowduck.be/posts/id-design-and-primary-keys-pt-1"/>
    <content type="html">&lt;blockquote&gt;
&lt;p&gt;Is your database design stunted by traditional approaches? Alexey Makhotkin delves into a refreshing take on primary keys and ID design. He highlights the importance of aligning primary key selection with business requirements rather than sticking to conventional database norms. Through engaging examples, he introduces the concept of external IDs and anchor IDs, demonstrating how they relate to database entities. Makhotkin also emphasizes the significance of immutable anchor IDs for unique identification, plus the nuances of using external IDs effectively, even those from outside systems. By analyzing a minimal content management scenario, he shows the logical modeling with integer primary keys and uniqueness constraints, setting a strong foundation for understanding primary keys in database design. Part two promises to further explore the complexities of composite primary keys, making this a must-read for database enthusiasts.&lt;/p&gt;
&lt;/blockquote&gt;&lt;p&gt;&lt;a href=&quot;https://anchorsandlinks.com/posts/primary-keys/&quot;&gt;Continue reading on &lt;strong&gt;anchorsandlinks.com&lt;/strong&gt;&lt;/a&gt;&lt;p&gt;&lt;/p&gt;</content>
  </entry>
  <entry>
    <id>https://www.yellowduck.be/posts/natural-sorting-in-laravel-api-resources</id>
    <title>🐥 Natural sorting in Laravel API resources</title>
    <updated>2026-09-16T17:00:00Z</updated>
    <published>2026-09-16T17:00:00Z</published>
    <author><name>Pieter Claerhout</name><email>pieter@yellowduck.be</email></author>
    <link rel="alternate" href="https://www.yellowduck.be/posts/natural-sorting-in-laravel-api-resources"/>
    <content type="html">&lt;p&gt;When exposing related collections through Laravel API Resources, the default order is whatever the database returns — usually insertion order. For user-facing lists, that's rarely what you want.&lt;/p&gt;
&lt;h1&gt;The Problem&lt;/h1&gt;
&lt;p&gt;A resource with a &lt;code&gt;belongsToMany&lt;/code&gt; relation would return items in an arbitrary order:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-php&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;span class=&quot;l-line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;l-line&quot; data-line=&quot;2&quot;&gt;&lt;span class=&quot;l-string&quot;&gt;&amp;#39;items&amp;#39;&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;l-type&quot;&gt;ItemResource&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;l-function-call&quot;&gt;collection&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-variable-builtin&quot;&gt;$this&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;l-function-method-call&quot;&gt;whenLoaded&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;&amp;#39;items&amp;#39;&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;))&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Items named &amp;quot;Item 2&amp;quot; and &amp;quot;Item 10&amp;quot; would sort lexicographically: &lt;code&gt;Item 10&lt;/code&gt;, &lt;code&gt;Item 2&lt;/code&gt; — not what a user expects.&lt;/p&gt;
&lt;h1&gt;The Fix&lt;/h1&gt;
&lt;p&gt;&lt;code&gt;whenLoaded&lt;/code&gt; accepts a callback that runs only when the relation is loaded. Use it to sort the collection before handing it off to the resource:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-php&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;span class=&quot;l-line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;l-line&quot; data-line=&quot;2&quot;&gt;&lt;span class=&quot;l-string&quot;&gt;&amp;#39;items&amp;#39;&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;l-type&quot;&gt;ItemResource&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;l-function-call&quot;&gt;collection&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;3&quot;&gt;    &lt;span class=&quot;l-variable-builtin&quot;&gt;$this&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;l-function-method-call&quot;&gt;whenLoaded&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;&amp;#39;items&amp;#39;&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-keyword-function&quot;&gt;fn&lt;/span&gt; &lt;span class=&quot;l-punctuation-bracket&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;l-variable-builtin&quot;&gt;$this&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;l-variable-member&quot;&gt;items&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;l-function-method-call&quot;&gt;sortBy&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;&amp;#39;name&amp;#39;&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-constant&quot;&gt;SORT_NATURAL&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;l-constant&quot;&gt;SORT_FLAG_CASE&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;))&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;4&quot;&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;SORT_NATURAL&lt;/code&gt; gives you human-friendly ordering (&lt;code&gt;Item 1&lt;/code&gt;, &lt;code&gt;Item 2&lt;/code&gt;, &lt;code&gt;Item 10&lt;/code&gt;). &lt;code&gt;SORT_FLAG_CASE&lt;/code&gt; makes it case-insensitive. The callback is only invoked when the relation is loaded, so lazy-loading is never triggered accidentally.&lt;/p&gt;
&lt;h1&gt;Testing It&lt;/h1&gt;
&lt;p&gt;The key assertion is that numeric suffixes sort numerically, not lexicographically:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-php&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;span class=&quot;l-line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;l-line&quot; data-line=&quot;2&quot;&gt;&lt;span class=&quot;l-variable&quot;&gt;$item10&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;l-type&quot;&gt;Item&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;l-function-call&quot;&gt;factory&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;()&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;l-function-method-call&quot;&gt;create&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;([&lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;&amp;#39;name&amp;#39;&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;&amp;#39;Item 10&amp;#39;&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;])&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;3&quot;&gt;&lt;span class=&quot;l-variable&quot;&gt;$item2&lt;/span&gt;  &lt;span class=&quot;l-operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;l-type&quot;&gt;Item&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;l-function-call&quot;&gt;factory&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;()&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;l-function-method-call&quot;&gt;create&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;([&lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;&amp;#39;name&amp;#39;&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;&amp;#39;Item 2&amp;#39;&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;])&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;4&quot;&gt;&lt;span class=&quot;l-variable&quot;&gt;$item1&lt;/span&gt;  &lt;span class=&quot;l-operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;l-type&quot;&gt;Item&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;l-function-call&quot;&gt;factory&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;()&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;l-function-method-call&quot;&gt;create&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;([&lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;&amp;#39;name&amp;#39;&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;&amp;#39;Item 1&amp;#39;&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;])&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;5&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;6&quot;&gt;&lt;span class=&quot;l-variable&quot;&gt;$model&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;l-function-method-call&quot;&gt;items&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;()&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;l-function-method-call&quot;&gt;attach&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;([&lt;/span&gt;&lt;span class=&quot;l-variable&quot;&gt;$item10&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;l-variable-member&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-variable&quot;&gt;$item2&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;l-variable-member&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-variable&quot;&gt;$item1&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;l-variable-member&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;])&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;7&quot;&gt;&lt;span class=&quot;l-variable&quot;&gt;$model&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;l-function-method-call&quot;&gt;load&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;&amp;#39;items&amp;#39;&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;8&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;9&quot;&gt;&lt;span class=&quot;l-variable&quot;&gt;$result&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;l-constructor&quot;&gt;ModelResource&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-variable&quot;&gt;$model&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;))&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;l-function-method-call&quot;&gt;toArray&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-type&quot;&gt;Request&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;l-function-call&quot;&gt;create&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;&amp;#39;/&amp;#39;&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;))&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;10&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;11&quot;&gt;&lt;span class=&quot;l-variable&quot;&gt;$names&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;l-function-call&quot;&gt;collect&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-variable&quot;&gt;$result&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;&amp;#39;items&amp;#39;&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;])&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;l-function-method-call&quot;&gt;pluck&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;&amp;#39;name&amp;#39;&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;l-function-method-call&quot;&gt;values&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;()&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;l-function-method-call&quot;&gt;all&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;()&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;12&quot;&gt;&lt;span class=&quot;l-variable-builtin&quot;&gt;$this&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;l-function-method-call&quot;&gt;assertSame&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;([&lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;&amp;#39;Item 1&amp;#39;&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;&amp;#39;Item 2&amp;#39;&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;&amp;#39;Item 10&amp;#39;&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-variable&quot;&gt;$names&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Attach them out of order, load, assert the right order comes out. Simple and robust.&lt;/p&gt;&lt;p&gt;&lt;a href=&quot;https://www.yellowduck.be/tags/laravel&quot;&gt;#laravel&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/php&quot;&gt;#php&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/best-practice&quot;&gt;#best-practice&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/http&quot;&gt;#http&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/testing&quot;&gt;#testing&lt;/a&gt;&lt;/p&gt;</content>
  </entry>
  <entry>
    <id>https://www.yellowduck.be/posts/undoing-a-pushed-merge-commit-without-losing-work</id>
    <title>🐥 Undoing a pushed merge commit without losing work</title>
    <updated>2026-09-12T17:00:00Z</updated>
    <published>2026-09-12T17:00:00Z</published>
    <author><name>Pieter Claerhout</name><email>pieter@yellowduck.be</email></author>
    <link rel="alternate" href="https://www.yellowduck.be/posts/undoing-a-pushed-merge-commit-without-losing-work"/>
    <content type="html">&lt;p&gt;Sometimes you push a merge commit you didn't mean to, and notice it only after the fact. Here's how we handled it cleanly when that happened on a feature branch.&lt;/p&gt;
&lt;p&gt;My branch history looked like this:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-plaintext&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;span class=&quot;l-line&quot; data-line=&quot;1&quot;&gt;abc1234  Fix for the feature we were working on  ← latest commit, must keep&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;2&quot;&gt;def5678  Merge branch &amp;#39;feature/my-branch&amp;#39; of ...  ← mistake&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;3&quot;&gt;ghi9012  Previous commit&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;4&quot;&gt;jkl3456  Older commit&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The merge commit &lt;code&gt;def5678&lt;/code&gt; was already pushed to GitHub. The latest fix (&lt;code&gt;abc1234&lt;/code&gt;) sat on top of it, so a plain &lt;code&gt;git reset --hard HEAD~1&lt;/code&gt; would have taken the fix with it.&lt;/p&gt;
&lt;p&gt;I needed to drop exactly one commit from the middle of the history while keeping everything above it. &lt;code&gt;git rebase --onto&lt;/code&gt; is the right tool for this:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-bash&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;span class=&quot;l-line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;l-function-call&quot;&gt;git&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;rebase&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;--onto&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;ghi9012&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;def5678&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;HEAD&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This tells Git: take all commits after &lt;code&gt;def5678&lt;/code&gt; up to &lt;code&gt;HEAD&lt;/code&gt;, and replay them directly onto &lt;code&gt;ghi9012&lt;/code&gt; — the commit before the bad merge. The merge commit is skipped entirely.&lt;/p&gt;
&lt;p&gt;The rebase left us in a detached HEAD state, so we updated the branch pointer and switched back:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-bash&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;span class=&quot;l-line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;l-function-call&quot;&gt;git&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;branch&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;-f&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;feature/my-branch&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;HEAD&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;2&quot;&gt;&lt;span class=&quot;l-function-call&quot;&gt;git&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;checkout&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;feature/my-branch&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The history was now clean:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-plaintext&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;span class=&quot;l-line&quot; data-line=&quot;1&quot;&gt;xyz7890  Fix for the feature we were working on&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;2&quot;&gt;ghi9012  Previous commit&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;3&quot;&gt;jkl3456  Older commit&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then a force-push to update the remote:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-bash&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;span class=&quot;l-line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;l-function-call&quot;&gt;git&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;push&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;--force&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Why not &lt;code&gt;git revert&lt;/code&gt;?&lt;/p&gt;
&lt;p&gt;&lt;code&gt;git revert -m 1 &amp;lt;hash&amp;gt;&lt;/code&gt; is the safer choice when multiple people have already pulled the branch, because it adds a new commit rather than rewriting history. In our case the branch was a personal feature branch with no other active collaborators, so rewriting was fine and kept the history cleaner.&lt;/p&gt;
&lt;p&gt;When in doubt on a shared branch, revert. On a solo feature branch, rebase is cleaner.&lt;/p&gt;&lt;p&gt;&lt;a href=&quot;https://www.yellowduck.be/tags/best-practice&quot;&gt;#best-practice&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/terminal&quot;&gt;#terminal&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/git&quot;&gt;#git&lt;/a&gt;&lt;/p&gt;</content>
  </entry>
  <entry>
    <id>https://www.yellowduck.be/posts/laravel-boost-best-practices-a-major-tone-shift-august-2026</id>
    <title>🐥 Laravel boost best practices: A major tone shift (August 2026)</title>
    <updated>2026-09-07T17:00:00Z</updated>
    <published>2026-09-07T17:00:00Z</published>
    <author><name>Pieter Claerhout</name><email>pieter@yellowduck.be</email></author>
    <link rel="alternate" href="https://www.yellowduck.be/posts/laravel-boost-best-practices-a-major-tone-shift-august-2026"/>
    <content type="html">&lt;p&gt;Two commits landed in the &lt;a href=&quot;https://github.com/laravel/boost&quot;&gt;Laravel Boost&lt;/a&gt; repository on August 26, 2026 that significantly update the AI guidance rules baked into the package. The changes touch all 19 best-practice rule files and add a new section on controller resource design. Here is what changed and why it matters.&lt;/p&gt;
&lt;h1&gt;The big picture: from prescriptive to contextual&lt;/h1&gt;
&lt;p&gt;The first commit (&lt;a href=&quot;https://github.com/laravel/boost/commit/d165b9ea6086294a8f1999ac3dcf4e61da267b23&quot;&gt;&lt;code&gt;d165b9ea&lt;/code&gt;&lt;/a&gt;) touches 1,427 lines across every rule file. The headline change is not a new feature — it is a &lt;strong&gt;deliberate softening of absolute language&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Rules that previously said &lt;strong&gt;&amp;quot;Incorrect&amp;quot; / &amp;quot;Correct&amp;quot;&lt;/strong&gt; now use labels like:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;em&gt;&amp;quot;Manual lookup:&amp;quot;&lt;/em&gt; / &lt;em&gt;&amp;quot;Use route model binding:&amp;quot;&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;&amp;quot;Hidden dependency:&amp;quot;&lt;/em&gt; / &lt;em&gt;&amp;quot;Injected dependency:&amp;quot;&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;&amp;quot;Unsafe:&amp;quot;&lt;/em&gt; / &lt;em&gt;&amp;quot;Preferred:&amp;quot;&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;&amp;quot;Queued and durable:&amp;quot;&lt;/em&gt; / &lt;em&gt;&amp;quot;Deferred in the current process:&amp;quot;&lt;/em&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The intent is clear: the guidance now presents &lt;strong&gt;trade-offs and context&lt;/strong&gt; instead of moral verdicts. This matters because AI coding agents (and developers) following these rules were previously being trained to treat &amp;quot;Incorrect&amp;quot; examples as always wrong, even in situations where the pattern was actually fine.&lt;/p&gt;
&lt;h1&gt;Key Changes by Area&lt;/h1&gt;
&lt;h2&gt;Queries — &lt;code&gt;advanced-queries.md&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;The old guidance said &lt;code&gt;whereHas()&lt;/code&gt; was flat-out wrong because it &amp;quot;re-executes per row.&amp;quot; The new version is more honest:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;whereHas()&lt;/code&gt; typically produces an &lt;code&gt;EXISTS&lt;/code&gt; subquery, while &lt;code&gt;whereIn()&lt;/code&gt; can express the same filter with an &lt;code&gt;IN&lt;/code&gt; subquery. Either form may be faster depending on the database engine, indexes, cardinality, and query plan. &lt;strong&gt;Measure both forms with representative data.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The section title changed from &lt;em&gt;&amp;quot;Prefer &lt;code&gt;whereIn&lt;/code&gt; + Subquery Over &lt;code&gt;whereHas&lt;/code&gt;&amp;quot;&lt;/em&gt; to &lt;em&gt;&amp;quot;Compare &lt;code&gt;whereHas()&lt;/code&gt; with an &lt;code&gt;IN&lt;/code&gt; Subquery&amp;quot;&lt;/em&gt;. Similarly, the compound index rule now says &lt;em&gt;&amp;quot;verify the query plan&amp;quot;&lt;/em&gt; rather than asserting a single correct approach.&lt;/p&gt;
&lt;p&gt;A technical fix also snuck in: the dynamic relationship example now correctly passes the foreign key to &lt;code&gt;belongsTo()&lt;/code&gt;:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-php&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;span class=&quot;l-line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;l-line&quot; data-line=&quot;2&quot;&gt;&lt;span class=&quot;l-comment&quot;&gt;// Before (broken — missing the FK argument):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;3&quot;&gt;&lt;span class=&quot;l-keyword-return&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;l-variable-builtin&quot;&gt;$this&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;l-function-method-call&quot;&gt;belongsTo&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-type&quot;&gt;Login&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;l-constant&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;4&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;5&quot;&gt;&lt;span class=&quot;l-comment&quot;&gt;// After (correct):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;6&quot;&gt;&lt;span class=&quot;l-keyword-return&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;l-variable-builtin&quot;&gt;$this&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;l-function-method-call&quot;&gt;belongsTo&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-type&quot;&gt;Login&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;l-constant&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;&amp;#39;last_login_id&amp;#39;&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Architecture — &lt;code&gt;architecture.md&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;Two notable shifts:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Dependency injection&lt;/strong&gt;: The old rule said constructor injection was always correct and method injection was a code smell. The new rule aligns with how Laravel actually works:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Prefer constructor injection for dependencies required throughout an object's lifetime. &lt;strong&gt;Method injection is appropriate for dependencies needed by one controller action, listener, job handler, or other container-invoked method.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;Default sort order&lt;/strong&gt;: Instead of &amp;quot;always use &lt;code&gt;latest()&lt;/code&gt;&amp;quot;, the guidance now recommends a stable two-column sort with a tie-breaker for reliable pagination:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-php&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;span class=&quot;l-line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;l-line&quot; data-line=&quot;2&quot;&gt;&lt;span class=&quot;l-type&quot;&gt;Post&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;l-function-call&quot;&gt;query&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;()&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;3&quot;&gt;    &lt;span class=&quot;l-operator&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;l-function-method-call&quot;&gt;orderByDesc&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;&amp;#39;created_at&amp;#39;&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;4&quot;&gt;    &lt;span class=&quot;l-operator&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;l-function-method-call&quot;&gt;orderByDesc&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;&amp;#39;id&amp;#39;&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;5&quot;&gt;    &lt;span class=&quot;l-operator&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;l-function-method-call&quot;&gt;paginate&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;()&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Caching — &lt;code&gt;caching.md&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;The &lt;code&gt;Cache::remember()&lt;/code&gt; section now explicitly warns about the false-cache-miss bug:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;The manual version below incorrectly treats valid falsy values, such as &lt;code&gt;false&lt;/code&gt; or &lt;code&gt;0&lt;/code&gt;, as cache misses.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;It also adds an honest caveat: &lt;code&gt;Cache::remember()&lt;/code&gt; does not prevent concurrent requests from computing the same missing value — you still need &lt;code&gt;Cache::lock()&lt;/code&gt; for that.&lt;/p&gt;
&lt;h2&gt;Routing — &lt;code&gt;routing.md&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;The &amp;quot;Keep Controllers Thin&amp;quot; rule (with its arbitrary &amp;quot;under 10 lines&amp;quot; target) is gone. In its place:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Controllers should coordinate HTTP input, authorization, validation, an application operation, and the response. &lt;strong&gt;Extract substantial or reusable business logic, but do not introduce an action or service merely to satisfy an arbitrary line limit.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;Migrations — &lt;code&gt;migrations.md&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;The file shrank significantly (78 → 24 deletions vs. 24 additions). Several overly prescriptive examples were removed. The foreign-key section now includes a practical note:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Do not add a duplicate single-column index without checking the database driver's treatment of foreign-key indexes and the indexes already created by the migration.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;Security — &lt;code&gt;security.md&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;Two nuanced additions stand out:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;$guarded = []&lt;/code&gt; is no longer labeled universally &amp;quot;Incorrect.&amp;quot; The new text acknowledges that mass-assignment protection controls &lt;em&gt;which attributes can be set&lt;/em&gt;, not values or authorization — and that deliberate conventions using &lt;code&gt;$guarded&lt;/code&gt; are valid.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;The SQL injection example now uses the fluent string helper properly:&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-php&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;span class=&quot;l-line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;l-line&quot; data-line=&quot;2&quot;&gt;&lt;span class=&quot;l-type&quot;&gt;User&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;l-function-call&quot;&gt;whereRaw&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;&amp;#39;LOWER(name) = ?&amp;#39;&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;l-variable&quot;&gt;$request&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;l-function-method-call&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;&amp;#39;name&amp;#39;&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;l-function-method-call&quot;&gt;lower&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;()&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;l-function-method-call&quot;&gt;toString&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;()])&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;l-function-method-call&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;()&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;ol start=&quot;3&quot;&gt;
&lt;li&gt;A new explicit note: &lt;em&gt;&amp;quot;Public actions intentionally available to everyone do not need a redundant authorization check.&amp;quot;&lt;/em&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Queues — &lt;code&gt;queue-jobs.md&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;The Amazon SQS footnote is new and important:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Amazon Simple Queue Service uses its &lt;strong&gt;visibility timeout&lt;/strong&gt; instead of Laravel's &lt;code&gt;retry_after&lt;/code&gt;; configure that timeout at the queue level.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The &lt;code&gt;ShouldBeUnique&lt;/code&gt; section now notes that all dispatching processes must share a cache store that supports locks — a silent gotcha that has burned teams before.&lt;/p&gt;
&lt;hr /&gt;
&lt;h1&gt;New section: organize controllers around resources&lt;/h1&gt;
&lt;p&gt;The second commit (&lt;a href=&quot;https://github.com/laravel/boost/commit/d165b9ea6086294a8f1999ac3dcf4e61da267b23&quot;&gt;&lt;code&gt;b19e98a8&lt;/code&gt;&lt;/a&gt;) adds a standalone section to &lt;code&gt;routing.md&lt;/code&gt; covering a pattern that often gets debated on Laravel teams.&lt;/p&gt;
&lt;p&gt;The guidance establishes a clear default: organize controllers around one resource using standard resource actions (&lt;code&gt;index&lt;/code&gt;, &lt;code&gt;show&lt;/code&gt;, &lt;code&gt;create&lt;/code&gt;, &lt;code&gt;store&lt;/code&gt;, &lt;code&gt;edit&lt;/code&gt;, &lt;code&gt;update&lt;/code&gt;, &lt;code&gt;destroy&lt;/code&gt;). When a custom verb like &lt;code&gt;publish&lt;/code&gt;, &lt;code&gt;approve&lt;/code&gt;, or &lt;code&gt;archive&lt;/code&gt; appears, treat it as a &lt;strong&gt;design signal&lt;/strong&gt; — it might represent a separate resource.&lt;/p&gt;
&lt;p&gt;The canonical example models podcast publishing as a dedicated controller:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-php&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;span class=&quot;l-line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;l-line&quot; data-line=&quot;2&quot;&gt;&lt;span class=&quot;l-comment&quot;&gt;// Route&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;3&quot;&gt;&lt;span class=&quot;l-type&quot;&gt;Route&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;l-function-call&quot;&gt;post&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;&amp;#39;/published-podcasts/{podcast}&amp;#39;&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;l-type&quot;&gt;PublishedPodcastController&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;l-constant&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;&amp;#39;store&amp;#39;&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;])&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;4&quot;&gt;    &lt;span class=&quot;l-operator&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;l-function-method-call&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;&amp;#39;published-podcasts.store&amp;#39;&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;5&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;6&quot;&gt;&lt;span class=&quot;l-type&quot;&gt;Route&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;l-function-call&quot;&gt;delete&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;&amp;#39;/published-podcasts/{podcast}&amp;#39;&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;l-type&quot;&gt;PublishedPodcastController&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;l-constant&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;&amp;#39;destroy&amp;#39;&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;])&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;7&quot;&gt;    &lt;span class=&quot;l-operator&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;l-function-method-call&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;&amp;#39;published-podcasts.destroy&amp;#39;&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;8&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;9&quot;&gt;&lt;span class=&quot;l-comment&quot;&gt;// Controller&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;10&quot;&gt;&lt;span class=&quot;l-keyword-type&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;l-type&quot;&gt;PublishedPodcastController&lt;/span&gt; &lt;span class=&quot;l-keyword&quot;&gt;extends&lt;/span&gt; &lt;span class=&quot;l-type&quot;&gt;Controller&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;11&quot;&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;12&quot;&gt;    &lt;span class=&quot;l-keyword-modifier&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;l-keyword-function&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;l-function-method&quot;&gt;store&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-type&quot;&gt;Podcast&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;$podcast&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;l-type&quot;&gt;RedirectResponse&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;13&quot;&gt;    &lt;span class=&quot;l-punctuation-bracket&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;14&quot;&gt;        &lt;span class=&quot;l-variable&quot;&gt;$podcast&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;l-function-method-call&quot;&gt;publish&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;()&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;15&quot;&gt;        &lt;span class=&quot;l-keyword-return&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;l-function-call&quot;&gt;back&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;()&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;16&quot;&gt;    &lt;span class=&quot;l-punctuation-bracket&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;17&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;18&quot;&gt;    &lt;span class=&quot;l-keyword-modifier&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;l-keyword-function&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;l-function-method&quot;&gt;destroy&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-type&quot;&gt;Podcast&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;$podcast&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;l-type&quot;&gt;RedirectResponse&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;19&quot;&gt;    &lt;span class=&quot;l-punctuation-bracket&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;20&quot;&gt;        &lt;span class=&quot;l-variable&quot;&gt;$podcast&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;l-function-method-call&quot;&gt;unpublish&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;()&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;21&quot;&gt;        &lt;span class=&quot;l-keyword-return&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;l-function-call&quot;&gt;back&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;()&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;22&quot;&gt;    &lt;span class=&quot;l-punctuation-bracket&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;23&quot;&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The guidance ends with an important escape hatch:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Treat a custom verb as a &lt;strong&gt;design signal, not proof&lt;/strong&gt; that another controller is required. Use query parameters for simple filtering, and keep an explicit action route when modeling the operation as a resource would obscure the domain or conflict with established project conventions.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;Why this matters for AI-assisted development&lt;/h2&gt;
&lt;p&gt;Laravel Boost feeds these rules directly to AI coding agents as context when generating code. Overly prescriptive &amp;quot;always/never&amp;quot; rules cause agents to reject valid patterns and to add unnecessary abstractions (action classes for trivial logic, for example). The new tone should lead to more pragmatic, context-aware suggestions — and fewer unnecessary &amp;quot;Incorrect&amp;quot; rewrites of perfectly reasonable code.&lt;/p&gt;
&lt;p&gt;The shift also signals a broader direction: good AI guidance reads like a &lt;strong&gt;senior developer explaining trade-offs&lt;/strong&gt;, not a linter listing violations.&lt;/p&gt;&lt;p&gt;&lt;a href=&quot;https://www.yellowduck.be/tags/laravel&quot;&gt;#laravel&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/best-practice&quot;&gt;#best-practice&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/ai&quot;&gt;#ai&lt;/a&gt;&lt;/p&gt;</content>
  </entry>
  <entry>
    <id>https://www.yellowduck.be/posts/one-line-to-measure-php-memory</id>
    <title>🐥 One line to measure PHP memory</title>
    <updated>2026-09-05T17:00:00Z</updated>
    <published>2026-09-05T17:00:00Z</published>
    <author><name>Pieter Claerhout</name><email>pieter@yellowduck.be</email></author>
    <link rel="alternate" href="https://www.yellowduck.be/posts/one-line-to-measure-php-memory"/>
    <content type="html">&lt;p&gt;You've just run a long-running Artisan command and production is complaining about memory. Before reaching for Xdebug or a full profiler, there's a tool already on your machine that answers the question in one line:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-bash&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;span class=&quot;l-line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;l-function-call&quot;&gt;/usr/bin/time&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;-l&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;php&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;artisan&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;your:command&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;2&amp;gt;&amp;amp;&lt;/span&gt;&lt;span class=&quot;l-number&quot;&gt;1&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;l-function-call&quot;&gt;grep&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;&amp;quot;maximum resident&amp;quot;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That's it. No code changes, no instrumentation, no deployment.&lt;/p&gt;
&lt;h2&gt;Breaking it down&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;/usr/bin/time&lt;/code&gt;&lt;/strong&gt; — the system binary, not the shell built-in. The full path is important: your shell likely has a &lt;code&gt;time&lt;/code&gt; built-in that doesn't support the &lt;code&gt;-l&lt;/code&gt; flag.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;-l&lt;/code&gt;&lt;/strong&gt; — tells it to emit detailed resource usage. On Linux, use &lt;code&gt;-v&lt;/code&gt; instead.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;2&amp;gt;&amp;amp;1&lt;/code&gt;&lt;/strong&gt; — &lt;code&gt;time&lt;/code&gt; writes its output to stderr, so you need to redirect it into stdout before you can pipe it.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;| grep &amp;quot;maximum resident&amp;quot;&lt;/code&gt;&lt;/strong&gt; — filters down to the one line you care about.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The output looks like this:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-plaintext&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;span class=&quot;l-line&quot; data-line=&quot;1&quot;&gt; 147922944  maximum resident set size&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That number is in bytes. Divide by &lt;code&gt;1024 × 1024&lt;/code&gt; and you have the peak RSS in megabytes — in this case, &lt;strong&gt;141 MB&lt;/strong&gt;. That's the high-water mark of physical RAM the process held at any one moment, and the right number to compare against &lt;code&gt;memory_limit&lt;/code&gt; in &lt;code&gt;php.ini&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;Why not &lt;code&gt;memory_get_peak_usage()&lt;/code&gt;?&lt;/h2&gt;
&lt;p&gt;PHP's own &lt;code&gt;memory_get_peak_usage(true)&lt;/code&gt; is great when you control the code — add it at the end of &lt;code&gt;handle()&lt;/code&gt; and you see PHP's internal allocator peak. But it only sees what PHP's memory manager tracked. The OS-level approach catches everything: the runtime itself, loaded extensions, forked child processes. For commands that spawn subprocesses or load large extensions, the two numbers can diverge meaningfully.&lt;/p&gt;
&lt;p&gt;The OS measurement also requires no deployment — run it against production code on a staging server without touching a line.&lt;/p&gt;
&lt;h2&gt;macOS vs. Linux&lt;/h2&gt;
&lt;p&gt;Swap &lt;code&gt;-l&lt;/code&gt; for &lt;code&gt;-v&lt;/code&gt; on Linux. The field name also changes slightly — look for &lt;code&gt;&amp;quot;Maximum resident set size (kbytes)&amp;quot;&lt;/code&gt; — and note the unit shift from bytes to kilobytes.&lt;/p&gt;
&lt;h2&gt;Drop the grep for more&lt;/h2&gt;
&lt;p&gt;Skip the pipe entirely and &lt;code&gt;/usr/bin/time -l&lt;/code&gt; dumps the full resource table after your command finishes: page faults, context switches, I/O operations, wall time. Worth reading once to understand what your command is actually doing at the OS level.&lt;/p&gt;&lt;p&gt;&lt;a href=&quot;https://www.yellowduck.be/tags/laravel&quot;&gt;#laravel&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/php&quot;&gt;#php&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/best-practice&quot;&gt;#best-practice&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/terminal&quot;&gt;#terminal&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/sysadmin&quot;&gt;#sysadmin&lt;/a&gt;&lt;/p&gt;</content>
  </entry>
  <entry>
    <id>https://www.yellowduck.be/posts/customizing-the-laravel-boost-guidelines</id>
    <title>🐥 Customizing the Laravel Boost guidelines</title>
    <updated>2026-09-02T17:00:00Z</updated>
    <published>2026-09-02T17:00:00Z</published>
    <author><name>Pieter Claerhout</name><email>pieter@yellowduck.be</email></author>
    <link rel="alternate" href="https://www.yellowduck.be/posts/customizing-the-laravel-boost-guidelines"/>
    <content type="html">&lt;p&gt;In my current project, I'm using &lt;a href=&quot;https://laravel.com/ai/boost&quot;&gt;Laravel Boost&lt;/a&gt; to injects contextual guidelines into your AI coding assistant (Claude Code, Cursor, Copilot, etc.) by composing a set of named rule blocks — &lt;code&gt;foundation&lt;/code&gt;, &lt;code&gt;boost&lt;/code&gt;, &lt;code&gt;php&lt;/code&gt;, &lt;code&gt;laravel/core&lt;/code&gt;, &lt;code&gt;deployments&lt;/code&gt;, and version-specific blocks like &lt;code&gt;laravel/v13&lt;/code&gt; — into the &lt;code&gt;CLAUDE.md&lt;/code&gt; file at the root of your project.&lt;/p&gt;
&lt;p&gt;One of the things that annoyed me is that &lt;a href=&quot;https://github.com/laravel/boost/pull/758&quot;&gt;this PR&lt;/a&gt; added marketing info from Laravel promoting their Laravel Cloud solution for deployment. As I'm not using it (and have no intentions to do so), after each update of the Boost guidelines, I needed to manually remove this part:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-markdown&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;span class=&quot;l-line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;l-markup-heading-1&quot;&gt;# Deployment&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;2&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;3&quot;&gt;&lt;span class=&quot;l-markup-list&quot;&gt;- &lt;/span&gt;Laravel can be deployed using &lt;span class=&quot;l-markup-link&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;l-markup-link-label&quot;&gt;Laravel Cloud&lt;/span&gt;&lt;span class=&quot;l-markup-link&quot;&gt;](&lt;/span&gt;&lt;span class=&quot;l-markup-link-url&quot;&gt;https://cloud.laravel.com/&lt;/span&gt;&lt;span class=&quot;l-markup-link&quot;&gt;)&lt;/span&gt;, which is the fastest way to deploy and scale production Laravel applications.&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It turns out there is another PR that offers a solution for this, &lt;a href=&quot;https://github.com/laravel/boost/pull/774&quot;&gt;PR #774&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;It adds a small but useful quality-of-life improvement: the deployment guideline has been extracted from &lt;code&gt;laravel/core&lt;/code&gt; into its own named block (&lt;code&gt;deployments&lt;/code&gt;), and you can now exclude any named guideline block from being injected via a config option.&lt;/p&gt;
&lt;p&gt;To exclude these guidelines, first start with publishing the Boost config if you haven't already:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-bash&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;span class=&quot;l-line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;l-function-call&quot;&gt;php&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;artisan&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;vendor:publish&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;--tag=boost-config&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then add the &lt;code&gt;guidelines&lt;/code&gt; key to &lt;code&gt;config/boost.php&lt;/code&gt;:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-php&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;span class=&quot;l-line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;l-line&quot; data-line=&quot;2&quot;&gt;&lt;span class=&quot;l-keyword-return&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;l-punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;3&quot;&gt;    &lt;span class=&quot;l-comment&quot;&gt;// ...&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;4&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;5&quot;&gt;    &lt;span class=&quot;l-string&quot;&gt;&amp;#39;guidelines&amp;#39;&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;l-punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;6&quot;&gt;        &lt;span class=&quot;l-string&quot;&gt;&amp;#39;exclude&amp;#39;&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;l-punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;7&quot;&gt;            &lt;span class=&quot;l-string&quot;&gt;&amp;#39;deployments&amp;#39;&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;8&quot;&gt;        &lt;span class=&quot;l-punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;9&quot;&gt;    &lt;span class=&quot;l-punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;10&quot;&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;After the next &lt;code&gt;boost:update&lt;/code&gt; run, the &lt;code&gt;=== deployments rules ===&lt;/code&gt; block will no longer appear in your &lt;code&gt;CLAUDE.md&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;AI coding assistants consume every line of your guidelines as context on every request. Irrelevant rules waste tokens and can subtly steer the model toward unhelpful suggestions. If your team deploys to a custom pipeline, Kubernetes, or anything other than Laravel Cloud, the default deployment hint is pure noise — and now you can cleanly remove it without patching vendor files.&lt;/p&gt;
&lt;p&gt;The same &lt;code&gt;exclude&lt;/code&gt; mechanism works for any named guideline block, so as Boost gains more optional sections (versioned package rules, framework-specific hints), you'll be able to keep your injected context lean and project-relevant.&lt;/p&gt;
&lt;p&gt;If you want to keep deployment guidance but tailor it to your stack, the PR also documents the override path: create &lt;code&gt;.ai/deployments/core.blade.php&lt;/code&gt; in your project root and write your own instructions there. Boost picks up local overrides before falling back to its own templates, so your custom block replaces the default without needing the &lt;code&gt;exclude&lt;/code&gt; config at all.&lt;/p&gt;&lt;p&gt;&lt;a href=&quot;https://www.yellowduck.be/tags/laravel&quot;&gt;#laravel&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/php&quot;&gt;#php&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/ai&quot;&gt;#ai&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/devops&quot;&gt;#devops&lt;/a&gt;&lt;/p&gt;</content>
  </entry>
  <entry>
    <id>https://www.yellowduck.be/posts/building-a-laravel-style-event-system-in-elixir-phoenix</id>
    <title>🐥 Building a Laravel-style event system in Elixir Phoenix</title>
    <updated>2026-08-31T17:00:00Z</updated>
    <published>2026-08-31T17:00:00Z</published>
    <author><name>Pieter Claerhout</name><email>pieter@yellowduck.be</email></author>
    <link rel="alternate" href="https://www.yellowduck.be/posts/building-a-laravel-style-event-system-in-elixir-phoenix"/>
    <content type="html">&lt;p&gt;Laravel's events and listeners are a fantastic way to decouple your application. You dispatch an event, register one or more listeners, and each listener reacts independently. It's a simple pattern that keeps your code clean.&lt;/p&gt;
&lt;p&gt;Phoenix doesn't provide an equivalent abstraction out of the box, but OTP and Phoenix PubSub make it straightforward to build something similar while remaining idiomatic.&lt;/p&gt;
&lt;h1&gt;Publishing domain events&lt;/h1&gt;
&lt;p&gt;Rather than having models emit events automatically, publish events from your contexts after a successful database operation.&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-elixir&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;span class=&quot;l-line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;l-keyword-function&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;l-function&quot;&gt;update_user&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-variable&quot;&gt;user&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-variable&quot;&gt;attrs&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;l-keyword&quot;&gt;do&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;2&quot;&gt;  &lt;span class=&quot;l-variable&quot;&gt;old&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;l-variable&quot;&gt;user&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;3&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;4&quot;&gt;  &lt;span class=&quot;l-keyword&quot;&gt;with&lt;/span&gt; &lt;span class=&quot;l-punctuation-bracket&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;l-string-special-symbol&quot;&gt;:ok&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-variable&quot;&gt;user&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;&amp;lt;-&lt;/span&gt; &lt;span class=&quot;l-module&quot;&gt;User&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;l-function-call&quot;&gt;changeset&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-variable&quot;&gt;user&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-variable&quot;&gt;attrs&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;|&amp;gt;&lt;/span&gt; &lt;span class=&quot;l-module&quot;&gt;Repo&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;l-function-call&quot;&gt;update&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;l-keyword&quot;&gt;do&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;5&quot;&gt;    &lt;span class=&quot;l-module&quot;&gt;Events&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;l-function-call&quot;&gt;publish&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-punctuation-special&quot;&gt;%&lt;/span&gt;&lt;span class=&quot;l-module&quot;&gt;Events.UserUpdated&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;6&quot;&gt;      &lt;span class=&quot;l-string-special-symbol&quot;&gt;old: &lt;/span&gt;&lt;span class=&quot;l-variable&quot;&gt;old&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;7&quot;&gt;      &lt;span class=&quot;l-string-special-symbol&quot;&gt;new: &lt;/span&gt;&lt;span class=&quot;l-variable&quot;&gt;user&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;8&quot;&gt;      &lt;span class=&quot;l-string-special-symbol&quot;&gt;actor_id: &lt;/span&gt;&lt;span class=&quot;l-variable&quot;&gt;attrs&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;l-string-special-symbol&quot;&gt;:actor_id&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;9&quot;&gt;    &lt;span class=&quot;l-punctuation-bracket&quot;&gt;})&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;10&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;11&quot;&gt;    &lt;span class=&quot;l-punctuation-bracket&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;l-string-special-symbol&quot;&gt;:ok&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-variable&quot;&gt;user&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;12&quot;&gt;  &lt;span class=&quot;l-keyword&quot;&gt;end&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;13&quot;&gt;&lt;span class=&quot;l-keyword&quot;&gt;end&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Notice that the event describes a business action (&lt;code&gt;UserUpdated&lt;/code&gt;) rather than a database operation.&lt;/p&gt;
&lt;h1&gt;Creating an event bus&lt;/h1&gt;
&lt;p&gt;A small wrapper around Phoenix PubSub provides a central place for publishing events.&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-elixir&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;span class=&quot;l-line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;l-keyword-function&quot;&gt;defmodule&lt;/span&gt; &lt;span class=&quot;l-module&quot;&gt;MyApp.Events&lt;/span&gt; &lt;span class=&quot;l-keyword-function&quot;&gt;do&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;2&quot;&gt;  &lt;span class=&quot;l-operator&quot;&gt;&lt;span class=&quot;l-constant&quot;&gt;@&lt;span class=&quot;l-function-call&quot;&gt;&lt;span class=&quot;l-constant&quot;&gt;topic &lt;span class=&quot;l-string&quot;&gt;&amp;quot;events&amp;quot;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;3&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;4&quot;&gt;  &lt;span class=&quot;l-keyword-function&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;l-function&quot;&gt;publish&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-variable&quot;&gt;event&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;l-keyword&quot;&gt;do&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;5&quot;&gt;    &lt;span class=&quot;l-module&quot;&gt;Phoenix.PubSub&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;l-function-call&quot;&gt;broadcast&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;6&quot;&gt;      &lt;span class=&quot;l-module&quot;&gt;MyApp.PubSub&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;7&quot;&gt;      &lt;span class=&quot;l-operator&quot;&gt;&lt;span class=&quot;l-constant&quot;&gt;@&lt;span class=&quot;l-constant&quot;&gt;topic&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;8&quot;&gt;      &lt;span class=&quot;l-punctuation-bracket&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;l-string-special-symbol&quot;&gt;:event&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-variable&quot;&gt;event&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;9&quot;&gt;    &lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;10&quot;&gt;  &lt;span class=&quot;l-keyword&quot;&gt;end&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;11&quot;&gt;&lt;span class=&quot;l-keyword&quot;&gt;end&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This gives you an API similar to Laravel's &lt;code&gt;event(...)&lt;/code&gt; helper.&lt;/p&gt;
&lt;h1&gt;Listening for events&lt;/h1&gt;
&lt;p&gt;Listeners are simply GenServers that subscribe to your event topic.&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-elixir&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;span class=&quot;l-line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;l-keyword-function&quot;&gt;defmodule&lt;/span&gt; &lt;span class=&quot;l-module&quot;&gt;MyApp.Audit.Listener&lt;/span&gt; &lt;span class=&quot;l-keyword-function&quot;&gt;do&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;2&quot;&gt;  &lt;span class=&quot;l-keyword&quot;&gt;use&lt;/span&gt; &lt;span class=&quot;l-module&quot;&gt;GenServer&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;3&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;4&quot;&gt;  &lt;span class=&quot;l-keyword-function&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;l-function&quot;&gt;start_link&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-comment&quot;&gt;_&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;l-keyword&quot;&gt;do&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;5&quot;&gt;    &lt;span class=&quot;l-module&quot;&gt;GenServer&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;l-function-call&quot;&gt;start_link&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-constant-builtin&quot;&gt;__MODULE__&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-punctuation-special&quot;&gt;%&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;{})&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;6&quot;&gt;  &lt;span class=&quot;l-keyword&quot;&gt;end&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;7&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;8&quot;&gt;  &lt;span class=&quot;l-keyword-function&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;l-function&quot;&gt;init&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-variable&quot;&gt;state&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;l-keyword&quot;&gt;do&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;9&quot;&gt;    &lt;span class=&quot;l-module&quot;&gt;Phoenix.PubSub&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;l-function-call&quot;&gt;subscribe&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-module&quot;&gt;MyApp.PubSub&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;&amp;quot;events&amp;quot;&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;10&quot;&gt;    &lt;span class=&quot;l-punctuation-bracket&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;l-string-special-symbol&quot;&gt;:ok&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-variable&quot;&gt;state&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;11&quot;&gt;  &lt;span class=&quot;l-keyword&quot;&gt;end&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;12&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;13&quot;&gt;  &lt;span class=&quot;l-keyword-function&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;l-function&quot;&gt;handle_info&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;({&lt;/span&gt;&lt;span class=&quot;l-string-special-symbol&quot;&gt;:event&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-variable&quot;&gt;event&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-variable&quot;&gt;state&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;l-keyword&quot;&gt;do&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;14&quot;&gt;    &lt;span class=&quot;l-function-call&quot;&gt;handle_event&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-variable&quot;&gt;event&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;15&quot;&gt;    &lt;span class=&quot;l-punctuation-bracket&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;l-string-special-symbol&quot;&gt;:noreply&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-variable&quot;&gt;state&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;16&quot;&gt;  &lt;span class=&quot;l-keyword&quot;&gt;end&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;17&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;18&quot;&gt;  &lt;span class=&quot;l-keyword-function&quot;&gt;defp&lt;/span&gt; &lt;span class=&quot;l-function&quot;&gt;handle_event&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-punctuation-special&quot;&gt;%&lt;/span&gt;&lt;span class=&quot;l-module&quot;&gt;Events.UserUpdated&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;{}&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;l-variable&quot;&gt;event&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;l-keyword&quot;&gt;do&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;19&quot;&gt;    &lt;span class=&quot;l-module&quot;&gt;Audit&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;l-function-call&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-variable&quot;&gt;event&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;20&quot;&gt;  &lt;span class=&quot;l-keyword&quot;&gt;end&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;21&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;22&quot;&gt;  &lt;span class=&quot;l-keyword-function&quot;&gt;defp&lt;/span&gt; &lt;span class=&quot;l-function&quot;&gt;handle_event&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-comment&quot;&gt;_&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-string-special-symbol&quot;&gt;do: :ok&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;23&quot;&gt;&lt;span class=&quot;l-keyword&quot;&gt;end&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Adding additional listeners for emails, metrics, search indexing or webhooks is simply a matter of starting another GenServer.&lt;/p&gt;
&lt;h1&gt;Is this suitable for audit logging?&lt;/h1&gt;
&lt;p&gt;Not quite.&lt;/p&gt;
&lt;p&gt;Phoenix PubSub is excellent for notifying other parts of your application, but it does not guarantee that a listener has successfully processed an event.&lt;/p&gt;
&lt;p&gt;Imagine the following sequence:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-plaintext&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;span class=&quot;l-line&quot; data-line=&quot;1&quot;&gt;Repo.update(...)&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;2&quot;&gt;↓&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;3&quot;&gt;Publish event&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;4&quot;&gt;↓&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;5&quot;&gt;Application crashes&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The database change has been committed, but the audit record may never be written.&lt;/p&gt;
&lt;p&gt;For an audit trail, that's usually unacceptable.&lt;/p&gt;
&lt;h1&gt;Use Ecto.Multi for audit logging&lt;/h1&gt;
&lt;p&gt;A more robust approach is to store the audit record in the same transaction as the database change.&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-elixir&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;span class=&quot;l-line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;l-module&quot;&gt;Ecto.Multi&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;l-function-call&quot;&gt;new&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;()&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;2&quot;&gt;&lt;span class=&quot;l-operator&quot;&gt;|&amp;gt;&lt;/span&gt; &lt;span class=&quot;l-module&quot;&gt;Ecto.Multi&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;l-function-call&quot;&gt;update&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-string-special-symbol&quot;&gt;:user&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-variable&quot;&gt;changeset&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;3&quot;&gt;&lt;span class=&quot;l-operator&quot;&gt;|&amp;gt;&lt;/span&gt; &lt;span class=&quot;l-module&quot;&gt;Ecto.Multi&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;l-function-call&quot;&gt;insert&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-string-special-symbol&quot;&gt;:audit_log&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-keyword&quot;&gt;fn&lt;/span&gt; &lt;span class=&quot;l-punctuation-special&quot;&gt;%&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;l-string-special-symbol&quot;&gt;user: &lt;/span&gt;&lt;span class=&quot;l-variable&quot;&gt;user&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;4&quot;&gt;  &lt;span class=&quot;l-module&quot;&gt;AuditLog&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;l-function-call&quot;&gt;changeset&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-punctuation-special&quot;&gt;%&lt;/span&gt;&lt;span class=&quot;l-module&quot;&gt;AuditLog&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;{}&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-punctuation-special&quot;&gt;%&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;5&quot;&gt;    &lt;span class=&quot;l-string-special-symbol&quot;&gt;entity: &lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;&amp;quot;user&amp;quot;&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;6&quot;&gt;    &lt;span class=&quot;l-string-special-symbol&quot;&gt;entity_id: &lt;/span&gt;&lt;span class=&quot;l-variable&quot;&gt;user&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;l-variable&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;7&quot;&gt;    &lt;span class=&quot;l-string-special-symbol&quot;&gt;action: &lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;&amp;quot;updated&amp;quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;8&quot;&gt;  &lt;span class=&quot;l-punctuation-bracket&quot;&gt;})&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;9&quot;&gt;&lt;span class=&quot;l-keyword&quot;&gt;end&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;10&quot;&gt;&lt;span class=&quot;l-operator&quot;&gt;|&amp;gt;&lt;/span&gt; &lt;span class=&quot;l-module&quot;&gt;Repo&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;l-function-call&quot;&gt;transaction&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;()&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now either both records are committed, or neither is.&lt;/p&gt;
&lt;h1&gt;Capturing the changes&lt;/h1&gt;
&lt;p&gt;You don't need to manually compare two structs. Ecto already tracks modified fields through the changeset.&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-elixir&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;span class=&quot;l-line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;l-variable&quot;&gt;changeset&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;l-variable&quot;&gt;changes&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For more detailed audit logs, you can combine the original struct with the updated one to produce a before/after representation.&lt;/p&gt;
&lt;p&gt;For example:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-json&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;span class=&quot;l-line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;2&quot;&gt;  &lt;span class=&quot;l-property&quot;&gt;&amp;quot;name&amp;quot;&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;l-punctuation-bracket&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;3&quot;&gt;    &lt;span class=&quot;l-property&quot;&gt;&amp;quot;old&amp;quot;&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;&amp;quot;John&amp;quot;&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;4&quot;&gt;    &lt;span class=&quot;l-property&quot;&gt;&amp;quot;new&amp;quot;&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;&amp;quot;Johnny&amp;quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;5&quot;&gt;  &lt;span class=&quot;l-punctuation-bracket&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;6&quot;&gt;  &lt;span class=&quot;l-property&quot;&gt;&amp;quot;email&amp;quot;&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;l-punctuation-bracket&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;7&quot;&gt;    &lt;span class=&quot;l-property&quot;&gt;&amp;quot;old&amp;quot;&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;&amp;quot;john@example.com&amp;quot;&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;8&quot;&gt;    &lt;span class=&quot;l-property&quot;&gt;&amp;quot;new&amp;quot;&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;&amp;quot;johnny@example.com&amp;quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;9&quot;&gt;  &lt;span class=&quot;l-punctuation-bracket&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;10&quot;&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This makes it easy to display exactly what changed in an audit history.&lt;/p&gt;
&lt;h1&gt;Putting it together&lt;/h1&gt;
&lt;p&gt;A pattern that has worked well for larger Phoenix applications is to separate business events from auditing:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Publish domain events with Phoenix PubSub for emails, search indexing, webhooks and other asynchronous work.&lt;/li&gt;
&lt;li&gt;Persist audit logs inside the same database transaction using &lt;code&gt;Ecto.Multi&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Offload expensive work to Oban jobs instead of executing it directly from listeners.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This gives you the same loose coupling as Laravel's event system while embracing Elixir's strengths: explicit processes, OTP supervision and reliable transactional guarantees.&lt;/p&gt;&lt;p&gt;&lt;a href=&quot;https://www.yellowduck.be/tags/pattern&quot;&gt;#pattern&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/laravel&quot;&gt;#laravel&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/database&quot;&gt;#database&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/elixir&quot;&gt;#elixir&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/phoenix&quot;&gt;#phoenix&lt;/a&gt;&lt;/p&gt;</content>
  </entry>
  <entry>
    <id>https://www.yellowduck.be/posts/chaining-github-actions-workflows-the-right-way</id>
    <title>🐥 Chaining GitHub Actions Workflows the right way</title>
    <updated>2026-08-25T17:00:00Z</updated>
    <published>2026-08-25T17:00:00Z</published>
    <author><name>Pieter Claerhout</name><email>pieter@yellowduck.be</email></author>
    <link rel="alternate" href="https://www.yellowduck.be/posts/chaining-github-actions-workflows-the-right-way"/>
    <content type="html">&lt;p&gt;If you've spent any time maintaining CI/CD pipelines, you've probably run into this situation: a workflow that deploys your app, and some follow-up work that should happen right after — posting a changelog, syncing release notes to a project tracker, notifying a downstream system. The naive solution is to pile everything into the same workflow file. It works, but it doesn't stay clean for long.&lt;/p&gt;
&lt;p&gt;A better option is to split the follow-up into its own workflow file and call it from the deployment workflow when needed. GitHub Actions supports this natively through reusable workflows, and once you understand how the wiring works, it's surprisingly straightforward.&lt;/p&gt;
&lt;p&gt;GitHub Actions has a trigger type called &lt;code&gt;workflow_call&lt;/code&gt;. When you add it to a workflow's &lt;code&gt;on:&lt;/code&gt; block, that workflow becomes callable by other workflows in the same repository — similar to importing a function from another module. The calling workflow references it as a job using &lt;code&gt;uses:&lt;/code&gt; with a relative path, instead of a &lt;code&gt;runs-on&lt;/code&gt; + &lt;code&gt;steps&lt;/code&gt; structure.&lt;/p&gt;
&lt;p&gt;The pattern looks like this:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The reusable workflow&lt;/strong&gt; (&lt;code&gt;release-notes.yaml&lt;/code&gt;):&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-yaml&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;span class=&quot;l-line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;l-property&quot;&gt;on&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;2&quot;&gt;  &lt;span class=&quot;l-property&quot;&gt;workflow_call&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;3&quot;&gt;  &lt;span class=&quot;l-property&quot;&gt;workflow_dispatch&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;:&lt;/span&gt;  &lt;span class=&quot;l-comment&quot;&gt;# keep this so you can still trigger it manually&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;The caller workflow&lt;/strong&gt; (&lt;code&gt;deploy.yaml&lt;/code&gt;):&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-yaml&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;span class=&quot;l-line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;l-property&quot;&gt;jobs&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;2&quot;&gt;  &lt;span class=&quot;l-property&quot;&gt;deploy&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;3&quot;&gt;    &lt;span class=&quot;l-property&quot;&gt;runs-on&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;ubuntu-latest&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;4&quot;&gt;    &lt;span class=&quot;l-property&quot;&gt;steps&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;5&quot;&gt;      &lt;span class=&quot;l-punctuation-delimiter&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;l-property&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;Deploy&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;6&quot;&gt;        &lt;span class=&quot;l-property&quot;&gt;run&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;l-function-call&quot;&gt;./deploy.sh&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;7&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;8&quot;&gt;  &lt;span class=&quot;l-property&quot;&gt;post-release-notes&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;9&quot;&gt;    &lt;span class=&quot;l-property&quot;&gt;needs&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;l-punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;deploy&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;10&quot;&gt;    &lt;span class=&quot;l-property&quot;&gt;uses&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;./.github/workflows/release-notes.yaml&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;11&quot;&gt;    &lt;span class=&quot;l-property&quot;&gt;secrets&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;inherit&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That's the whole thing. When the &lt;code&gt;deploy&lt;/code&gt; job finishes, &lt;code&gt;post-release-notes&lt;/code&gt; kicks off by running the full &lt;code&gt;release-notes.yaml&lt;/code&gt; workflow.&lt;/p&gt;
&lt;p&gt;A few things worth knowing&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Reusable workflows are jobs, not steps.&lt;/strong&gt; This is a constraint that trips people up. You cannot nest a &lt;code&gt;uses:&lt;/code&gt; directive inside a job's &lt;code&gt;steps:&lt;/code&gt; block. It has to be a top-level job. If you need to call a reusable workflow conditionally (e.g., only on pushes to &lt;code&gt;main&lt;/code&gt;), you do that with an &lt;code&gt;if:&lt;/code&gt; on the job itself, not inside the workflow being called.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;secrets: inherit&lt;/code&gt; does what you think.&lt;/strong&gt; Without it, the called workflow has no access to secrets. You'd have to pass them explicitly via &lt;code&gt;with:&lt;/code&gt; inputs, which is verbose. Using &lt;code&gt;secrets: inherit&lt;/code&gt; passes the calling workflow's secrets through automatically, which is almost always what you want for internal reuse within a single repo.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;The called workflow can still have its own triggers.&lt;/strong&gt; Adding &lt;code&gt;workflow_call&lt;/code&gt; doesn't replace whatever triggers were already there. A workflow that was previously only &lt;code&gt;schedule&lt;/code&gt;-triggered can become reusable without breaking the scheduled runs. This is handy during a migration — you can add &lt;code&gt;workflow_call&lt;/code&gt; first, wire up the caller, and verify everything works before removing any manual triggers.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Conditional execution on the caller side.&lt;/strong&gt; When you have separate deploy jobs for different environments (staging vs. production), you'll often want separate caller jobs too, each with its own &lt;code&gt;needs:&lt;/code&gt; and &lt;code&gt;if:&lt;/code&gt; condition:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-yaml&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;span class=&quot;l-line&quot; data-line=&quot;1&quot;&gt;  &lt;span class=&quot;l-property&quot;&gt;post-release-notes-staging&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;2&quot;&gt;    &lt;span class=&quot;l-property&quot;&gt;needs&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;l-punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;deploy-staging&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;3&quot;&gt;    &lt;span class=&quot;l-property&quot;&gt;if&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;github.ref == &amp;#39;refs/heads/develop&amp;#39;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;4&quot;&gt;    &lt;span class=&quot;l-property&quot;&gt;uses&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;./.github/workflows/release-notes.yaml&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;5&quot;&gt;    &lt;span class=&quot;l-property&quot;&gt;secrets&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;inherit&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;6&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;7&quot;&gt;  &lt;span class=&quot;l-property&quot;&gt;post-release-notes-production&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;8&quot;&gt;    &lt;span class=&quot;l-property&quot;&gt;needs&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;l-punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;deploy-production&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;9&quot;&gt;    &lt;span class=&quot;l-property&quot;&gt;if&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;github.ref == &amp;#39;refs/heads/main&amp;#39;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;10&quot;&gt;    &lt;span class=&quot;l-property&quot;&gt;uses&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;./.github/workflows/release-notes.yaml&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;11&quot;&gt;    &lt;span class=&quot;l-property&quot;&gt;secrets&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;inherit&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Each one runs independently after its respective deploy job, on the appropriate branch.&lt;/p&gt;
&lt;p&gt;You could trigger a workflow via the GitHub API with a &lt;code&gt;curl&lt;/code&gt; call to the &lt;code&gt;workflow_dispatch&lt;/code&gt; endpoint — and this is sometimes the right move when the target workflow lives in a different repository. But within the same repo, it's an awkward fit. You're introducing an async HTTP call where a direct dependency would do, you lose visibility into the triggered run from the calling workflow's summary page, and you have to handle auth tokens explicitly.&lt;/p&gt;
&lt;p&gt;With &lt;code&gt;workflow_call&lt;/code&gt;, the called workflow appears as a regular job in the same run. It shows up in the same Actions UI view, its logs are right there, and failures propagate naturally without any custom error handling.&lt;/p&gt;
&lt;p&gt;The main practical benefit of this approach isn't just reusability — it's that it lets you keep each workflow file focused on one thing. A deployment workflow shouldn't have to know about Jira, or Slack, or whatever notification system you're using. That logic belongs in its own file, tested and maintained separately, triggered wherever it's needed.&lt;/p&gt;
&lt;p&gt;Reusable workflows are one of the more underused features of GitHub Actions. The documentation covers them, but the examples tend to stop at &amp;quot;here's the syntax.&amp;quot; In practice, the pattern of splitting post-deployment tasks into callable workflows — and composing them from the main CI/CD pipeline — scales well as pipelines grow and keeps things readable as teams expand.&lt;/p&gt;
&lt;p&gt;If your deployment workflow is getting long, it's worth looking at what could be pulled out into a reusable workflow. Chances are, more of it is portable than you'd expect.&lt;/p&gt;&lt;p&gt;&lt;a href=&quot;https://www.yellowduck.be/tags/devops&quot;&gt;#devops&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/github&quot;&gt;#github&lt;/a&gt;&lt;/p&gt;</content>
  </entry>
  <entry>
    <id>https://www.yellowduck.be/posts/faster-leaner-mysql-backups-and-restores-with-mysqldump</id>
    <title>🐥 Faster, leaner MySQL backups and restores with mysqldump</title>
    <updated>2026-08-22T17:00:00Z</updated>
    <published>2026-08-22T17:00:00Z</published>
    <author><name>Pieter Claerhout</name><email>pieter@yellowduck.be</email></author>
    <link rel="alternate" href="https://www.yellowduck.be/posts/faster-leaner-mysql-backups-and-restores-with-mysqldump"/>
    <content type="html">&lt;p&gt;&lt;code&gt;mysqldump&lt;/code&gt; is a perfectly fine tool for many production workloads — until your database grows large enough that backups take too long and restores take even longer. Before you reach for a more exotic solution, there are several flags and session-level variables that can make a meaningful difference with zero infrastructure changes.&lt;/p&gt;
&lt;p&gt;This article focuses on plain MySQL, but shows where to wire up these settings when you use &lt;a href=&quot;https://github.com/spatie/laravel-backup&quot;&gt;spatie/laravel-backup&lt;/a&gt; and &lt;a href=&quot;https://github.com/stefanzweifel/laravel-backup-restore&quot;&gt;stefanzweifel/laravel-backup-restore&lt;/a&gt; in a Laravel project.&lt;/p&gt;
&lt;h1&gt;Where to configure this in Laravel&lt;/h1&gt;
&lt;p&gt;Both packages read dump and restore options from &lt;code&gt;config/database.php&lt;/code&gt;, inside the &lt;code&gt;dump&lt;/code&gt; key of your connection:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-php&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;span class=&quot;l-line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;l-line&quot; data-line=&quot;2&quot;&gt;&lt;span class=&quot;l-comment&quot;&gt;// config/database.php&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;3&quot;&gt;&lt;span class=&quot;l-string&quot;&gt;&amp;#39;mysql&amp;#39;&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;l-punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;4&quot;&gt;    &lt;span class=&quot;l-comment&quot;&gt;// ... standard connection settings ...&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;5&quot;&gt;    &lt;span class=&quot;l-string&quot;&gt;&amp;#39;dump&amp;#39;&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;l-punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;6&quot;&gt;        &lt;span class=&quot;l-string&quot;&gt;&amp;#39;excludeTables&amp;#39;&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;l-punctuation-bracket&quot;&gt;[&lt;/span&gt;...&lt;span class=&quot;l-punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;7&quot;&gt;        &lt;span class=&quot;l-string&quot;&gt;&amp;#39;useSingleTransaction&amp;#39;&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;l-boolean&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;8&quot;&gt;        &lt;span class=&quot;l-string&quot;&gt;&amp;#39;add_extra_option&amp;#39;&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;&amp;#39;...&amp;#39;&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-comment&quot;&gt;// passed to mysqldump (backup)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;9&quot;&gt;        &lt;span class=&quot;l-string&quot;&gt;&amp;#39;options&amp;#39;&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;&amp;#39;...&amp;#39;&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt;          &lt;span class=&quot;l-comment&quot;&gt;// passed to mysql client (restore)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;10&quot;&gt;    &lt;span class=&quot;l-punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;11&quot;&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;add_extra_option&lt;/code&gt; is forwarded to &lt;code&gt;mysqldump&lt;/code&gt; by spatie/laravel-backup.&lt;br /&gt;
&lt;code&gt;options&lt;/code&gt; is forwarded to the &lt;code&gt;mysql&lt;/code&gt; client by laravel-backup-restore.&lt;/p&gt;
&lt;p&gt;Everything below maps directly to one of those two keys.&lt;/p&gt;
&lt;h1&gt;Dump-time optimisations&lt;/h1&gt;
&lt;h2&gt;Exclude noisy, non-essential tables&lt;/h2&gt;
&lt;p&gt;Some tables accumulate millions of rows that are useful for debugging but worthless in a restore. Excluding them shrinks the dump file and cuts restore time dramatically.&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-php&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;span class=&quot;l-line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;l-line&quot; data-line=&quot;2&quot;&gt;&lt;span class=&quot;l-string&quot;&gt;&amp;#39;excludeTables&amp;#39;&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;l-punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;3&quot;&gt;    &lt;span class=&quot;l-string&quot;&gt;&amp;#39;telescope_entries&amp;#39;&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;4&quot;&gt;    &lt;span class=&quot;l-string&quot;&gt;&amp;#39;telescope_entries_tags&amp;#39;&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;5&quot;&gt;    &lt;span class=&quot;l-string&quot;&gt;&amp;#39;telescope_monitoring&amp;#39;&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;6&quot;&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Laravel Telescope is the classic example: it can easily outweigh the rest of your schema combined.&lt;/p&gt;
&lt;h2&gt;Use a single transaction&lt;/h2&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-plaintext&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;span class=&quot;l-line&quot; data-line=&quot;1&quot;&gt;--single-transaction&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In spatie/laravel-backup this is:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-php&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;span class=&quot;l-line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;l-line&quot; data-line=&quot;2&quot;&gt;&lt;span class=&quot;l-string&quot;&gt;&amp;#39;useSingleTransaction&amp;#39;&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;=&amp;gt;&lt;/span&gt; true&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This wraps the dump in a &lt;code&gt;START TRANSACTION&lt;/code&gt; so InnoDB tables are read from a consistent snapshot without locking them. Essential for any live database.&lt;/p&gt;
&lt;h2&gt;Increase &lt;code&gt;--net-buffer-length&lt;/code&gt;&lt;/h2&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-plaintext&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;span class=&quot;l-line&quot; data-line=&quot;1&quot;&gt;--net-buffer-length=16777216   # 16 MB (default is 1 MB)&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;mysqldump&lt;/code&gt; groups rows into multi-row &lt;code&gt;INSERT&lt;/code&gt; statements. The default maximum per statement is 1 MB. Raising it to 16 MB reduces the total number of statements in the file, which directly cuts parse and execution time during a restore.&lt;/p&gt;
&lt;p&gt;Pair it with &lt;code&gt;--max_allowed_packet&lt;/code&gt; on the server side:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-plaintext&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;span class=&quot;l-line&quot; data-line=&quot;1&quot;&gt;--max_allowed_packet=512M&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Without this, large packets are silently rejected.&lt;/p&gt;
&lt;h2&gt;Skip unnecessary LOCK/UNLOCK statements&lt;/h2&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-plaintext&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;span class=&quot;l-line&quot; data-line=&quot;1&quot;&gt;--skip-add-locks&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;mysqldump&lt;/code&gt; normally wraps each table dump in &lt;code&gt;LOCK TABLES … WRITE&lt;/code&gt; / &lt;code&gt;UNLOCK TABLES&lt;/code&gt;. This is redundant when you are already using &lt;code&gt;--single-transaction&lt;/code&gt; and when you disable table locking at restore time (see below). Removing these statements makes the dump file smaller and the restore faster.&lt;/p&gt;
&lt;h2&gt;Suppress MySQL 8 column statistics&lt;/h2&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-plaintext&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;span class=&quot;l-line&quot; data-line=&quot;1&quot;&gt;--column-statistics=0&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;MySQL 8's &lt;code&gt;mysqldump&lt;/code&gt; emits &lt;code&gt;ANALYZE TABLE&lt;/code&gt; statements to update column statistics after each table is imported. On large tables these can block progress noticeably. If you run your own statistics collection after a restore, suppress them.&lt;/p&gt;
&lt;h2&gt;Compress the dump on the wire&lt;/h2&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-plaintext&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;span class=&quot;l-line&quot; data-line=&quot;1&quot;&gt;--compression-algorithms=zlib&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Compresses data in transit between &lt;code&gt;mysqldump&lt;/code&gt; and the MySQL server. Useful when the client and server are on separate hosts, reducing network I/O. For socket connections the benefit is minimal, but there is no downside.&lt;/p&gt;
&lt;h2&gt;Skip binary log tracking (GTID)&lt;/h2&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-plaintext&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;span class=&quot;l-line&quot; data-line=&quot;1&quot;&gt;--set-gtid-purged=OFF&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If you use GTID-based replication, &lt;code&gt;mysqldump&lt;/code&gt; normally writes &lt;code&gt;SET @@GLOBAL.gtid_purged&lt;/code&gt; into the dump. During a restore this can conflict with an existing &lt;code&gt;gtid_executed&lt;/code&gt; set. Passing &lt;code&gt;OFF&lt;/code&gt; omits that statement and keeps restores clean on secondary or development databases.&lt;/p&gt;
&lt;h2&gt;Avoid table-level locks&lt;/h2&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-plaintext&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;span class=&quot;l-line&quot; data-line=&quot;1&quot;&gt;--lock-tables=false&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;When using &lt;code&gt;--single-transaction&lt;/code&gt;, table-level locking is already unnecessary. Explicitly disabling it avoids a redundant flush.&lt;/p&gt;
&lt;h2&gt;Use &lt;code&gt;--quick&lt;/code&gt; for large tables&lt;/h2&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-plaintext&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;span class=&quot;l-line&quot; data-line=&quot;1&quot;&gt;--quick&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;By default &lt;code&gt;mysqldump&lt;/code&gt; buffers an entire table in memory before writing. &lt;code&gt;--quick&lt;/code&gt; streams one row at a time, keeping memory usage flat regardless of table size. Always enable it for any production database.&lt;/p&gt;
&lt;h1&gt;Restore-time optimisations&lt;/h1&gt;
&lt;p&gt;These are passed to the &lt;code&gt;mysql&lt;/code&gt; client via an &lt;code&gt;--init-command&lt;/code&gt;, which MySQL executes before the dump is processed:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-sql&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;span class=&quot;l-line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;l-keyword&quot;&gt;SET&lt;/span&gt; &lt;span class=&quot;l-type&quot;&gt;sql_log_bin&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;l-number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;2&quot;&gt;&lt;span class=&quot;l-keyword&quot;&gt;SET&lt;/span&gt; &lt;span class=&quot;l-type&quot;&gt;foreign_key_checks&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;l-number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;3&quot;&gt;&lt;span class=&quot;l-keyword&quot;&gt;SET&lt;/span&gt; &lt;span class=&quot;l-type&quot;&gt;unique_checks&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;l-number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;4&quot;&gt;&lt;span class=&quot;l-keyword&quot;&gt;SET&lt;/span&gt; &lt;span class=&quot;l-type&quot;&gt;autocommit&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;l-number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In &lt;code&gt;config/database.php&lt;/code&gt;:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-php&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;span class=&quot;l-line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;l-line&quot; data-line=&quot;2&quot;&gt;&lt;span class=&quot;l-string&quot;&gt;&amp;#39;options&amp;#39;&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;&amp;#39;--init-command=&amp;quot;SET sql_log_bin=0; SET foreign_key_checks=0; SET unique_checks=0; SET autocommit=0;&amp;quot;&amp;#39;&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;&lt;code&gt;SET sql_log_bin=0&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;Disables binary logging for the session. There is no point logging every INSERT from a dump file into the binlog — it inflates the binlog and slows the restore. Safe when you control the import and are not relying on the binlog to propagate changes to replicas. This also reduces the amount of disk space needed for the restore.&lt;/p&gt;
&lt;h2&gt;&lt;code&gt;SET foreign_key_checks=0&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;MySQL enforces referential integrity row-by-row during inserts. In a full dump, the parent and child rows are all present — you just may not have inserted the parent yet when a child arrives. Disabling this check lets MySQL trust the dump and skip the per-row lookups, which is one of the most impactful restore optimisations available.&lt;/p&gt;
&lt;p&gt;Re-enable it (or restart the session) after the restore to verify the restored data is consistent.&lt;/p&gt;
&lt;h2&gt;&lt;code&gt;SET unique_checks=0&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;Similar rationale: MySQL normally verifies unique constraints on every insert. A correctly generated dump has no duplicates, so this check is redundant overhead. Disabling it reduces index update cost during the restore.&lt;/p&gt;
&lt;h2&gt;&lt;code&gt;SET autocommit=0&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;Turns off the implicit &lt;code&gt;COMMIT&lt;/code&gt; after every statement. Combined with the large &lt;code&gt;INSERT&lt;/code&gt; batches produced by &lt;code&gt;--net-buffer-length&lt;/code&gt;, this means the storage engine can batch many rows into a single transaction, dramatically reducing fsync pressure.&lt;/p&gt;
&lt;h1&gt;Putting it together&lt;/h1&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-php&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;span class=&quot;l-line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;l-line&quot; data-line=&quot;2&quot;&gt;&lt;span class=&quot;l-comment&quot;&gt;// config/database.php&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;3&quot;&gt;&lt;span class=&quot;l-string&quot;&gt;&amp;#39;dump&amp;#39;&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;l-punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;4&quot;&gt;    &lt;span class=&quot;l-string&quot;&gt;&amp;#39;excludeTables&amp;#39;&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;l-punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;5&quot;&gt;        &lt;span class=&quot;l-string&quot;&gt;&amp;#39;telescope_entries&amp;#39;&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;6&quot;&gt;        &lt;span class=&quot;l-string&quot;&gt;&amp;#39;telescope_entries_tags&amp;#39;&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;7&quot;&gt;        &lt;span class=&quot;l-string&quot;&gt;&amp;#39;telescope_monitoring&amp;#39;&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;8&quot;&gt;    &lt;span class=&quot;l-punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;9&quot;&gt;    &lt;span class=&quot;l-string&quot;&gt;&amp;#39;useSingleTransaction&amp;#39;&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;l-boolean&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;10&quot;&gt;    &lt;span class=&quot;l-string&quot;&gt;&amp;#39;add_extra_option&amp;#39;&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;&amp;#39;--set-gtid-purged=OFF --lock-tables=false --quick&amp;#39;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;11&quot;&gt;        &lt;span class=&quot;l-operator&quot;&gt;.&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;&amp;#39; --max_allowed_packet=512M --net-buffer-length=16777216&amp;#39;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;12&quot;&gt;        &lt;span class=&quot;l-operator&quot;&gt;.&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;&amp;#39; --compression-algorithms=zlib&amp;#39;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;13&quot;&gt;        &lt;span class=&quot;l-operator&quot;&gt;.&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;&amp;#39; --skip-add-locks --column-statistics=0&amp;#39;&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;14&quot;&gt;    &lt;span class=&quot;l-string&quot;&gt;&amp;#39;options&amp;#39;&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;&amp;#39;--init-command=&amp;quot;SET sql_log_bin=0; SET foreign_key_checks=0; SET unique_checks=0; SET autocommit=0;&amp;quot;&amp;#39;&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;15&quot;&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;These settings are safe for the vast majority of production MySQL 8 setups. The restore-side flags assume you trust the dump and will verify data integrity via application-level health checks afterwards — which is standard practice regardless of how you import.&lt;/p&gt;
&lt;p&gt;The backup side produces a smaller file, faster. The restore side skips redundant constraint checking and batches commits. Together they can reduce both phases by a significant margin without changing anything about your infrastructure or your backup tooling.&lt;/p&gt;
&lt;h1&gt;More info&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://spatie.be/docs/laravel-backup/v10/introduction&quot;&gt;Spatie Backup&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://stefanzweifel.dev/posts/2023/06/15/introducing-laravel-backup-restore/&quot;&gt;Introducing laravel-backup-restore
&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://dev.mysql.com/doc/refman/9.7/en/mysqldump.html&quot;&gt;mysqldump — A Database Backup Program&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://dev.mysql.com/doc/refman/9.7/en/mysql.html&quot;&gt;mysql — The MySQL Command-Line Client&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;&lt;a href=&quot;https://www.yellowduck.be/tags/laravel&quot;&gt;#laravel&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/best-practice&quot;&gt;#best-practice&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/mysql&quot;&gt;#mysql&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/devops&quot;&gt;#devops&lt;/a&gt;&lt;/p&gt;</content>
  </entry>
  <entry>
    <id>https://www.yellowduck.be/posts/speeding-up-our-php-test-suite-with-a-github-actions-matrix-strategy</id>
    <title>🐥 Speeding up our PHP test suite with a GitHub Actions matrix strategy</title>
    <updated>2026-08-20T17:00:00Z</updated>
    <published>2026-08-20T17:00:00Z</published>
    <author><name>Pieter Claerhout</name><email>pieter@yellowduck.be</email></author>
    <link rel="alternate" href="https://www.yellowduck.be/posts/speeding-up-our-php-test-suite-with-a-github-actions-matrix-strategy"/>
    <content type="html">&lt;p&gt;Our test suite had become a bottleneck. Every push to &lt;code&gt;develop&lt;/code&gt; or &lt;code&gt;main&lt;/code&gt; triggered three separate CI jobs — unit tests, feature tests, and integration tests — each running sequentially after the previous one finished and each downloading the same apt packages from scratch. The result: slow feedback loops and a lot of redundant work.&lt;/p&gt;
&lt;p&gt;This post walks through the changes we made to fix that.&lt;/p&gt;
&lt;h1&gt;The Problem: Three Jobs, Lots of Duplication&lt;/h1&gt;
&lt;p&gt;Before this change, the CI pipeline defined three independent jobs:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-yaml&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;span class=&quot;l-line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;l-property&quot;&gt;phpunit-unit&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;2&quot;&gt;  &lt;span class=&quot;l-property&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;&amp;quot;PHP Unit Tests&amp;quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;3&quot;&gt;  &lt;span class=&quot;l-property&quot;&gt;steps&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;4&quot;&gt;    &lt;span class=&quot;l-punctuation-delimiter&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;l-property&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;Install packages&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;5&quot;&gt;      &lt;span class=&quot;l-property&quot;&gt;run&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;|&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;6&quot;&gt;&lt;span class=&quot;l-string&quot;&gt;        &lt;span class=&quot;l-function-call&quot;&gt;sudo&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;apt&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;update&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;7&quot;&gt;&lt;span class=&quot;l-string&quot;&gt;        &lt;span class=&quot;l-function-call&quot;&gt;sudo&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;apt&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;install&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;-y&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;package-a&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;package-b&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;package-c&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;8&quot;&gt;    &lt;span class=&quot;l-punctuation-delimiter&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;l-property&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;Execute tests&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;9&quot;&gt;      &lt;span class=&quot;l-property&quot;&gt;run&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;l-function-call&quot;&gt;&lt;span class=&quot;l-string&quot;&gt;php&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;artisan&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;test&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;--parallel&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;tests/Unit&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;10&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;11&quot;&gt;&lt;span class=&quot;l-property&quot;&gt;phpunit-feature&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;12&quot;&gt;  &lt;span class=&quot;l-property&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;&amp;quot;PHP Feature Tests&amp;quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;13&quot;&gt;  &lt;span class=&quot;l-property&quot;&gt;steps&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;14&quot;&gt;    &lt;span class=&quot;l-comment&quot;&gt;# same apt install block...&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;15&quot;&gt;    &lt;span class=&quot;l-punctuation-delimiter&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;l-property&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;Execute tests&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;16&quot;&gt;      &lt;span class=&quot;l-property&quot;&gt;run&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;l-function-call&quot;&gt;&lt;span class=&quot;l-string&quot;&gt;php&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;artisan&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;test&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;--parallel&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;tests/Feature&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;17&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;18&quot;&gt;&lt;span class=&quot;l-property&quot;&gt;phpunit-integration&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;19&quot;&gt;  &lt;span class=&quot;l-property&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;&amp;quot;PHP Integration Tests&amp;quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;20&quot;&gt;  &lt;span class=&quot;l-property&quot;&gt;steps&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;21&quot;&gt;    &lt;span class=&quot;l-comment&quot;&gt;# same apt install block again...&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;22&quot;&gt;    &lt;span class=&quot;l-punctuation-delimiter&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;l-property&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;Execute tests&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;23&quot;&gt;      &lt;span class=&quot;l-property&quot;&gt;run&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;l-function-call&quot;&gt;&lt;span class=&quot;l-string&quot;&gt;php&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;artisan&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;test&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;--parallel&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;tests/Integration&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Each job was:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Checking out the repo&lt;/strong&gt; independently&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Setting up PHP&lt;/strong&gt; independently&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Installing Composer dependencies&lt;/strong&gt; independently (even with caching, cache restores take time)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Running &lt;code&gt;apt-get install&lt;/code&gt;&lt;/strong&gt; with the same packages — every single time, with no caching&lt;/li&gt;
&lt;/ol&gt;
&lt;h1&gt;Step 1: Collapsing Three Jobs into a Matrix&lt;/h1&gt;
&lt;p&gt;The first change replaced the three separate job definitions with a single &lt;code&gt;phpunit&lt;/code&gt; job that uses GitHub Actions' &lt;code&gt;matrix&lt;/code&gt; strategy:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-yaml&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;span class=&quot;l-line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;l-property&quot;&gt;phpunit&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;2&quot;&gt;  &lt;span class=&quot;l-property&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;&amp;quot;PHP Tests (${{ matrix.suite }})&amp;quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;3&quot;&gt;  &lt;span class=&quot;l-property&quot;&gt;runs-on&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;ubuntu-latest&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;4&quot;&gt;  &lt;span class=&quot;l-property&quot;&gt;strategy&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;5&quot;&gt;    &lt;span class=&quot;l-property&quot;&gt;fail-fast&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;l-boolean&quot;&gt;false&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;6&quot;&gt;    &lt;span class=&quot;l-property&quot;&gt;matrix&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;7&quot;&gt;      &lt;span class=&quot;l-property&quot;&gt;include&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;8&quot;&gt;        &lt;span class=&quot;l-punctuation-delimiter&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;l-property&quot;&gt;suite&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;unit-a&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;9&quot;&gt;        &lt;span class=&quot;l-punctuation-delimiter&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;l-property&quot;&gt;suite&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;unit-b&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;10&quot;&gt;        &lt;span class=&quot;l-punctuation-delimiter&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;l-property&quot;&gt;suite&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;feature-a&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;11&quot;&gt;        &lt;span class=&quot;l-punctuation-delimiter&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;l-property&quot;&gt;suite&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;feature-b&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;12&quot;&gt;        &lt;span class=&quot;l-punctuation-delimiter&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;l-property&quot;&gt;suite&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;Integration&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;All suites run in parallel, with &lt;code&gt;fail-fast: false&lt;/code&gt; so a failure in one suite doesn't cancel the others mid-run.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;Execute tests&lt;/code&gt; step became a single parameterised command:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-yaml&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;span class=&quot;l-line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;l-property&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;Execute tests&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;2&quot;&gt;  &lt;span class=&quot;l-property&quot;&gt;run&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;l-function-call&quot;&gt;&lt;span class=&quot;l-string&quot;&gt;php&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;artisan&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;test&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;--parallel&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;--testsuite=&lt;/span&gt;${{ matrix.suite &lt;span class=&quot;l-punctuation-bracket&quot;&gt;}}&lt;/span&gt; --coverage-clover test-coverage/coverage-${{ matrix.suite &lt;span class=&quot;l-punctuation-bracket&quot;&gt;}}&lt;/span&gt;&lt;span class=&quot;l-variable-parameter&quot;&gt;.xml&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Why More Suites Instead of Fewer?&lt;/h2&gt;
&lt;p&gt;The original split was coarse: all unit tests ran together, and all feature tests ran together. One large bucket would hold everything up even when most of the tests in it finished quickly.&lt;/p&gt;
&lt;p&gt;By naming suites explicitly in &lt;code&gt;phpunit.xml&lt;/code&gt;, we got finer-grained parallelism:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-xml&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;span class=&quot;l-line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;l-tag-delimiter&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;l-tag&quot;&gt;testsuite&lt;/span&gt; &lt;span class=&quot;l-tag-attribute&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;&amp;quot;&lt;/span&gt;unit-a&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;&amp;quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;l-tag-delimiter&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;2&quot;&gt;  &lt;span class=&quot;l-tag-delimiter&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;l-tag&quot;&gt;directory&lt;/span&gt; &lt;span class=&quot;l-tag-attribute&quot;&gt;suffix&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;&amp;quot;&lt;/span&gt;.php&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;&amp;quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;l-tag-delimiter&quot;&gt;&amp;gt;&lt;/span&gt;./tests/Unit/GroupA&lt;span class=&quot;l-tag-delimiter&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;l-tag&quot;&gt;directory&lt;/span&gt;&lt;span class=&quot;l-tag-delimiter&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;3&quot;&gt;&lt;span class=&quot;l-tag-delimiter&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;l-tag&quot;&gt;testsuite&lt;/span&gt;&lt;span class=&quot;l-tag-delimiter&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;4&quot;&gt;&lt;span class=&quot;l-tag-delimiter&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;l-tag&quot;&gt;testsuite&lt;/span&gt; &lt;span class=&quot;l-tag-attribute&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;&amp;quot;&lt;/span&gt;unit-b&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;&amp;quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;l-tag-delimiter&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;5&quot;&gt;  &lt;span class=&quot;l-tag-delimiter&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;l-tag&quot;&gt;directory&lt;/span&gt; &lt;span class=&quot;l-tag-attribute&quot;&gt;suffix&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;&amp;quot;&lt;/span&gt;.php&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;&amp;quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;l-tag-delimiter&quot;&gt;&amp;gt;&lt;/span&gt;./tests/Unit/GroupB&lt;span class=&quot;l-tag-delimiter&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;l-tag&quot;&gt;directory&lt;/span&gt;&lt;span class=&quot;l-tag-delimiter&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;6&quot;&gt;  &lt;span class=&quot;l-tag-delimiter&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;l-tag&quot;&gt;directory&lt;/span&gt; &lt;span class=&quot;l-tag-attribute&quot;&gt;suffix&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;&amp;quot;&lt;/span&gt;.php&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;&amp;quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;l-tag-delimiter&quot;&gt;&amp;gt;&lt;/span&gt;./tests/Unit/GroupC&lt;span class=&quot;l-tag-delimiter&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;l-tag&quot;&gt;directory&lt;/span&gt;&lt;span class=&quot;l-tag-delimiter&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;7&quot;&gt;&lt;span class=&quot;l-tag-delimiter&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;l-tag&quot;&gt;testsuite&lt;/span&gt;&lt;span class=&quot;l-tag-delimiter&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;8&quot;&gt;&lt;span class=&quot;l-tag-delimiter&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;l-tag&quot;&gt;testsuite&lt;/span&gt; &lt;span class=&quot;l-tag-attribute&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;&amp;quot;&lt;/span&gt;feature-a&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;&amp;quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;l-tag-delimiter&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;9&quot;&gt;  &lt;span class=&quot;l-tag-delimiter&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;l-tag&quot;&gt;directory&lt;/span&gt; &lt;span class=&quot;l-tag-attribute&quot;&gt;suffix&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;&amp;quot;&lt;/span&gt;.php&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;&amp;quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;l-tag-delimiter&quot;&gt;&amp;gt;&lt;/span&gt;./tests/Feature/GroupA&lt;span class=&quot;l-tag-delimiter&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;l-tag&quot;&gt;directory&lt;/span&gt;&lt;span class=&quot;l-tag-delimiter&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;10&quot;&gt;&lt;span class=&quot;l-tag-delimiter&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;l-tag&quot;&gt;testsuite&lt;/span&gt;&lt;span class=&quot;l-tag-delimiter&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;11&quot;&gt;&lt;span class=&quot;l-tag-delimiter&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;l-tag&quot;&gt;testsuite&lt;/span&gt; &lt;span class=&quot;l-tag-attribute&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;&amp;quot;&lt;/span&gt;feature-b&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;&amp;quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;l-tag-delimiter&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;12&quot;&gt;  &lt;span class=&quot;l-tag-delimiter&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;l-tag&quot;&gt;directory&lt;/span&gt; &lt;span class=&quot;l-tag-attribute&quot;&gt;suffix&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;&amp;quot;&lt;/span&gt;.php&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;&amp;quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;l-tag-delimiter&quot;&gt;&amp;gt;&lt;/span&gt;./tests/Feature/GroupB&lt;span class=&quot;l-tag-delimiter&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;l-tag&quot;&gt;directory&lt;/span&gt;&lt;span class=&quot;l-tag-delimiter&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;13&quot;&gt;  &lt;span class=&quot;l-tag-delimiter&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;l-tag&quot;&gt;directory&lt;/span&gt; &lt;span class=&quot;l-tag-attribute&quot;&gt;suffix&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;&amp;quot;&lt;/span&gt;.php&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;&amp;quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;l-tag-delimiter&quot;&gt;&amp;gt;&lt;/span&gt;./tests/Feature/GroupC&lt;span class=&quot;l-tag-delimiter&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;l-tag&quot;&gt;directory&lt;/span&gt;&lt;span class=&quot;l-tag-delimiter&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;14&quot;&gt;&lt;span class=&quot;l-tag-delimiter&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;l-tag&quot;&gt;testsuite&lt;/span&gt;&lt;span class=&quot;l-tag-delimiter&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This keeps each matrix leg roughly balanced in terms of test count and makes the overall wall-clock time shorter than when one slow bucket holds everything up.&lt;/p&gt;
&lt;h1&gt;Step 2: Caching apt Packages&lt;/h1&gt;
&lt;p&gt;The most wasteful part of the old setup was reinstalling the same system packages on every runner, for every job, from scratch. &lt;code&gt;apt-get install&lt;/code&gt; isn't slow in absolute terms, but multiplied across five parallel jobs and every CI run, it adds up.&lt;/p&gt;
&lt;p&gt;We replaced the raw &lt;code&gt;apt install&lt;/code&gt; block with &lt;a href=&quot;https://github.com/awalsh128/cache-apt-pkgs-action&quot;&gt;&lt;code&gt;awalsh128/cache-apt-pkgs-action&lt;/code&gt;&lt;/a&gt;:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-yaml&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;span class=&quot;l-line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;l-property&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;Install packages&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;2&quot;&gt;  &lt;span class=&quot;l-property&quot;&gt;uses&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;awalsh128/cache-apt-pkgs-action@latest&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;3&quot;&gt;  &lt;span class=&quot;l-property&quot;&gt;with&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;4&quot;&gt;    &lt;span class=&quot;l-property&quot;&gt;packages&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;package-a package-b package-c&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;5&quot;&gt;    &lt;span class=&quot;l-property&quot;&gt;version&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;${{ hashFiles(&amp;#39;.apt-packages&amp;#39;) }}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;version&lt;/code&gt; key is tied to a &lt;code&gt;.apt-packages&lt;/code&gt; lockfile committed to the repo. The cache is invalidated only when that file changes — not on every push — so the common case is a fast cache hit rather than a full install.&lt;/p&gt;
&lt;p&gt;Any post-install configuration that previously lived inside the install block becomes its own named step. The action calls it only on a cache miss, so it doesn't run unnecessarily on cache hits:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-yaml&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;span class=&quot;l-line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;l-property&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;Post-install configuration&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;2&quot;&gt;  &lt;span class=&quot;l-property&quot;&gt;run&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;l-comment&quot;&gt;# your configuration command here&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;The Downstream Effect: Coverage Merging&lt;/h1&gt;
&lt;p&gt;Any job that previously depended on the three separate job names now depends on the single &lt;code&gt;phpunit&lt;/code&gt; job:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-yaml&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;span class=&quot;l-line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;l-property&quot;&gt;some-downstream-job&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;2&quot;&gt;  &lt;span class=&quot;l-property&quot;&gt;needs&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;l-punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;3&quot;&gt;    &lt;span class=&quot;l-string&quot;&gt;&amp;quot;phpunit&amp;quot;&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt;   &lt;span class=&quot;l-comment&quot;&gt;# was: phpunit-unit, phpunit-feature, phpunit-integration&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;4&quot;&gt;    &lt;span class=&quot;l-string&quot;&gt;...&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;5&quot;&gt;  &lt;span class=&quot;l-punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And the coverage merge step includes one file per suite:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-bash&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;span class=&quot;l-line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;l-function-call&quot;&gt;./clover-merge&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;-o&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;coverage.xml&lt;/span&gt; \&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;2&quot;&gt;  &lt;span class=&quot;l-variable-parameter&quot;&gt;unit-a/coverage-unit-a.xml&lt;/span&gt; \&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;3&quot;&gt;  &lt;span class=&quot;l-variable-parameter&quot;&gt;unit-b/coverage-unit-b.xml&lt;/span&gt; \&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;4&quot;&gt;  &lt;span class=&quot;l-variable-parameter&quot;&gt;feature-a/coverage-feature-a.xml&lt;/span&gt; \&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;5&quot;&gt;  &lt;span class=&quot;l-variable-parameter&quot;&gt;feature-b/coverage-feature-b.xml&lt;/span&gt; \&lt;/span&gt;
&lt;span class=&quot;l-line&quot; data-line=&quot;6&quot;&gt;  &lt;span class=&quot;l-variable-parameter&quot;&gt;integration/coverage-integration.xml&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;What This Changes&lt;/h1&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Before&lt;/th&gt;
&lt;th&gt;After&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;3 jobs, one per test type&lt;/td&gt;
&lt;td&gt;5 jobs, all parallel&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;apt install&lt;/code&gt; on every run, every job&lt;/td&gt;
&lt;td&gt;Cached apt packages, invalidated only on lockfile change&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Coarse path-based test selection&lt;/td&gt;
&lt;td&gt;Named testsuites in &lt;code&gt;phpunit.xml&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;~3× duplicated job boilerplate&lt;/td&gt;
&lt;td&gt;Single parameterised job definition&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;The wall-clock time for the PHP test stage drops because the legs run concurrently and the apt install step is usually a cache hit. Maintenance is easier too — adding a new suite is a two-line change: one entry in &lt;code&gt;phpunit.xml&lt;/code&gt; and one entry in the matrix &lt;code&gt;include&lt;/code&gt; list.&lt;/p&gt;&lt;p&gt;&lt;a href=&quot;https://www.yellowduck.be/tags/php&quot;&gt;#php&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/devops&quot;&gt;#devops&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/github&quot;&gt;#github&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/testing&quot;&gt;#testing&lt;/a&gt;&lt;/p&gt;</content>
  </entry>
</feed>
