<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community</title>
    <description>The most recent home feed on DEV Community.</description>
    <link>https://dev.to</link>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/rss"/>
    <language>en</language>
    <item>
      <title>From "show me your code" to "show me your idea"</title>
      <dc:creator>zxpmail</dc:creator>
      <pubDate>Thu, 10 Sep 2026 22:42:34 +0000</pubDate>
      <link>https://dev.to/zxpmail/from-show-me-your-code-to-show-me-your-idea-30kk</link>
      <guid>https://dev.to/zxpmail/from-show-me-your-code-to-show-me-your-idea-30kk</guid>
      <description>&lt;h1&gt;
  
  
  From "show me your code" to "show me your idea"
&lt;/h1&gt;

&lt;p&gt;&lt;strong&gt;Or: the abdication is signed, and the court is empty&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;2026-07-13&lt;/em&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Second in a six-part series on what building with AI agents did to one developer. This piece is where code loses authority — and the question becomes what can still pin "this is what I meant."&lt;/p&gt;
&lt;/blockquote&gt;




&lt;p&gt;Last week someone turned a laptop toward me. A function on the screen, comments neat, tests green. He pointed at a line and said: this is what I meant.&lt;/p&gt;

&lt;p&gt;I asked: is that what you meant, or what the model smoothed into place?&lt;/p&gt;

&lt;p&gt;He froze. I froze. Three seconds opened in the air that neither of us would fill. In those three seconds I counted two thoughts. First: I shouldn't have asked. Second: who else is left to ask. Fingers on the trackpad edge — the heat was the machine's, not an idea's. Two people facing the same green, neither daring to hit Enter.&lt;/p&gt;

&lt;p&gt;It wasn't supposed to be like this — I thought. There used to be a tyrant: compiler, tests, exit codes. Talk is cheap, show me the code. Pretty talk died where the run failed. Drop the brick; fakes rang. After the ask you could stand across an exit code — a piece of iron in the middle that would not lie.&lt;/p&gt;

&lt;p&gt;The brick is still there. He pushed it over. The authority is gone. Running is no longer the same as "this is what you meant." The prettier the sample, the more the questioner looks like a nitpicker. And you still can't drop the question — drop it, and you pretend that running is the same as "this is what you meant."&lt;/p&gt;

&lt;p&gt;So some people bring it back: show me your talk. More meetings, spell it out, align the narrative. Talk was always cheap. I always took it for empty speech — time burned, energy burned — which is why I would rather stake the final court on code. Code lost its authority. I do not want that road back.&lt;/p&gt;

&lt;p&gt;So the ask shrinks to: show me your idea. Not inviting talk back in — wanting something that can still be pinned: a tradeoff, a fork, whose fault if wrong. Open your mouth, and it becomes talk again. Models say it too, often smoother. The waste I hated most came back in a different shirt. I am still beside him, trackpad heat still on my fingertip, not knowing whether to say "I believe you" or "prove it again." Both feel wrong. Swallow; throat goes dry.&lt;/p&gt;

&lt;p&gt;Two days later I asked him again. Softer: not "did you mean this," but "why write it this way." He answered fast. Boundaries first, then retry on failure, then why not the other path. Clean. I almost convicted him again for being too smooth. When he finished, I changed the knife — not restatement, a fork: "If the retry budget is cut in half, what breaks first?" He stopped. Not the pause of swapping connectives. After the stop, he named something that had not appeared in the first speech — the order of cache invalidation. That inch was his. A model can answer a fork too, often more completely. I asked again: if we do it in the order you said, which step blows up first online — which sentence do you own? He pointed at the one he had just spoken. That point mattered more than answering completely. I had been measuring wrong: smooth is not evidence of guilt; standing behind the fork is.&lt;/p&gt;

&lt;p&gt;I asked myself the same way. At night, facing a stretch I had just "written" — I had clicked Accept. The comment announced intent. I wiped the whole intent block, stared at the blank for a long time, then typed. What came out was almost as smooth as what I erased. Smooth enough to sicken me. Deleted again. Typed again. The third pass was still smooth. Finger on Save, I thought: if it writes smooth, does that mean it isn't mine. No. Reaching the same logic a second time can be called understanding. What I feared was not smooth — it was having no fork I could answer, while still using "I hesitated" to prove it was mine. I added a fork in my head, answered it poorly, and still pressed Save. The cursor moved on. I did not become more myself, or less.&lt;/p&gt;

&lt;p&gt;Later I thought: asking can move a person and still fail to pin a lie, so nail. Nail every claim into something that runs and shows a side effect. One night I nailed eleven. The ninth went red. I fixed it, ran again, red to green. While fixing, my hand was hot — briefly, like heat on loan. When all eleven went green, the terminal lit in a row. I sat with that row and waited for "this is what I meant" to sound inside. It didn't.&lt;/p&gt;

&lt;p&gt;That sit cleared the old books. An exit code only ever proved one thing: it runs. It never proved "this is what you meant." We treated those as the same passport and called the illusion rigor. The model did not remove the final court — it tore the illusion open. The green was steady. "This is what I meant" still did not sound. Nails can pin a lie. Nails cannot pin whose idea it was.&lt;/p&gt;

&lt;p&gt;I did not close his laptop. I did not rewrite that comment for him. The cursor still blinks after "this is what I meant." I no longer expect it to testify for anyone. What I can require is only a sentence that can bite me later: which tradeoff I own, and if it is wrong, it is mine. That sentence can live in a PR, or only in the mouth. What matters is not a tidy format — it is whether a person will still stand behind a choice.&lt;/p&gt;

&lt;p&gt;Those three seconds ended in stopping — stop asking green to prove both "it runs" and "this is what I meant."&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Previous in this series: &lt;a href="https://dev.to/zxpmail/the-mirror-cannot-reflect-thought-ig9"&gt;The Mirror Cannot Reflect Thought&lt;/a&gt; · To follow: Judging Fatigue — From Verifying AI to Verifying Myself · The Boundary of the Harness · A Reviewer Nailed Me in Six Places&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>aiagents</category>
      <category>writing</category>
      <category>essay</category>
    </item>
    <item>
      <title>J'ai procastiné Docker pendant un mois... Voici ce qui m'a débloquée</title>
      <dc:creator>SIEWE SANTHE AUDREY CAMILA</dc:creator>
      <pubDate>Thu, 10 Sep 2026 22:29:14 +0000</pubDate>
      <link>https://dev.to/audreysiewe14droid/jai-procastine-docker-pendant-un-mois-voici-ce-qui-ma-debloquee-1cdj</link>
      <guid>https://dev.to/audreysiewe14droid/jai-procastine-docker-pendant-un-mois-voici-ce-qui-ma-debloquee-1cdj</guid>
      <description>&lt;p&gt;Le 8 juillet, j'ai commencé un mentorat DevOps avec &lt;a href="https://dev.to/bongoe"&gt;Endah&lt;/a&gt;. J'étais motivée, pleine de bonnes résolutions, prête à attaquer le Docker comme une pro.&lt;/p&gt;

&lt;p&gt;Sauf qu'entre le 21 juillet et le 22 août, je n'ai rien publié. Mon dernier article ici datait du 29 juillet. Ce silence n'était pas de l'inaction : c'était la phase la moins visible de l'apprentissage, celle où on lit la documentation officielle, où on regarde des schémas d'architecture, où on essaie de comprendre &lt;em&gt;pourquoi&lt;/em&gt; Docker existe avant de taper la première commande. Le déclic est venu quand j'ai arrêté de vouloir tout comprendre en théorie avant de commencer, et que j'ai accepté d'apprendre en construisant.&lt;/p&gt;

&lt;p&gt;Si tu es en train de lire cet article en te reconnaissant un peu trop, reste. Ce texte est pour toi.&lt;/p&gt;

&lt;h2&gt;
  
  
  Le déclic : comprendre en construisant
&lt;/h2&gt;

&lt;p&gt;Ce qui m'a débloquée, ce n'est pas un énième tutoriel. C'est une idée toute bête : créer un conteneur qui fait quelque chose, même minuscule, pour voir concrètement ce que Docker permet de faire. J'ai appelé le projet "boxeur" ; une image basée sur Alpine qui exécute en boucle un script Bash et écrit des logs.&lt;/p&gt;

&lt;p&gt;Pas de grand plan au départ. Mais à mesure que j'avançais, les concepts clés de Docker se sont mis en place un par un : ce qu'est une image, ce qu'est un conteneur, comment ils communiquent, où vivent les données. Coder, lancer, observer ce qui casse, comprendre pourquoi c'est cette boucle qui m'a fait passer de "je regarde des vidéos sur Docker" à "je sais ce que je fais quand j'utilise Docker".&lt;/p&gt;

&lt;p&gt;C'est là que j'ai compris un principe qu'&lt;a href="https://dev.to/bongoe"&gt;Endah&lt;/a&gt; n'arrêtait pas de me répéter : sortir du tutorial hell. Regarder des vidéos, c'est confortable, mais ça reste passif. Taper &lt;code&gt;docker run&lt;/code&gt; pour la première fois, lire l'erreur que ton terminal te renvoie, et comprendre ce qu'elle signifie, c'est ça qui construit une vraie compréhension du système.&lt;/p&gt;

&lt;h2&gt;
  
  
  Les galères (et ce qu'elles m'ont appris sur Docker)
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Git 403 : mon premier mur
&lt;/h3&gt;

&lt;p&gt;Avant même de toucher à Docker, j'ai buté sur Git. Un bête &lt;code&gt;403 Forbidden&lt;/code&gt; en essayant de pousser mon code. La solution : générer un Personal Access Token sur GitHub, parce que les mots de passe classiques ne suffisent plus pour l'authentification en ligne de commande. Ça m'a forcée à comprendre comment GitHub gère l'authentification aujourd'hui, pas juste à copier une commande.&lt;/p&gt;

&lt;h3&gt;
  
  
  Des logs qui n'existaient pas
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;docker logs boxeur&lt;/code&gt;... rien. Un terminal désespérément vide. J'ai mis un moment à comprendre pourquoi : mon script écrivait ses logs dans un fichier à l'intérieur du conteneur, pas sur la sortie standard. Or Docker ne capture que ce qui sort sur stdout c'est une distinction fondamentale entre "écrire un fichier de log" et "logger au sens Docker du terme".&lt;/p&gt;

&lt;p&gt;J'ai donc utilisé une autre approche : lire le fichier directement depuis l'intérieur du conteneur.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker &lt;span class="nb"&gt;exec &lt;/span&gt;boxeur_mentor &lt;span class="nb"&gt;cat&lt;/span&gt; /var/log/boxeur.log

&lt;span class="o"&gt;![&lt;/span&gt; &lt;span class="o"&gt;](&lt;/span&gt;https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/r5wywzvdwbrqmveq1gni.png&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Et là, les logs sont apparus. Cette galère m'a appris comment Docker capture réellement les flux de sortie, et pourquoi la convention "logguer sur stdout" existe dans l'écosystème des conteneurs.&lt;/p&gt;

&lt;h3&gt;
  
  
  Alpine n'est pas Ubuntu
&lt;/h3&gt;

&lt;p&gt;J'ai voulu installer Nginx et j'ai tapé &lt;code&gt;apt-get install nginx&lt;/code&gt; par réflexe. Erreur immédiate. Alpine n'utilise pas &lt;code&gt;apt-get&lt;/code&gt;, mais son propre gestionnaire de paquets : &lt;code&gt;apk&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;apk add &lt;span class="nt"&gt;--no-cache&lt;/span&gt; nginx
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Petit détail, grande leçon : chaque image de base a son propre système et son propre gestionnaire de paquets. Ça m'a poussée à comprendre ce qui différencie réellement les distributions Linux utilisées comme images de base, plutôt que de copier des commandes sans en connaître le contexte.&lt;/p&gt;

&lt;p&gt;J'ai aussi bloqué sur un fichier &lt;code&gt;nginx.conf&lt;/code&gt; mal écrit (une erreur dès la ligne 2), qu'il a fallu déboguer et réécrire proprement pour que Nginx accepte de démarrer.&lt;/p&gt;

&lt;h3&gt;
  
  
  La faute de frappe qui m'a coûté une heure
&lt;/h3&gt;

&lt;p&gt;Au moment de pousser mon image sur Docker Hub, j'ai tagué mon image avec une majuscule : &lt;code&gt;Dreatech/boxeur&lt;/code&gt; au lieu de &lt;code&gt;dreatech/boxeur&lt;/code&gt;. Résultat, une erreur DNS aussi cryptique que frustrante :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;lookup Dreatech on 127.0.0.53:53: server misbehaving
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Docker Hub n'accepte pas les majuscules dans les noms de dépôt ; une convention de nommage héritée du système de registres de conteneurs. Une fois comprise, la correction était triviale :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker tag boxeur:latest dreatech/boxeur:latest
docker push dreatech/boxeur:latest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Ce que je retiens vraiment
&lt;/h2&gt;

&lt;p&gt;Au-delà des commandes, ce projet m'a permis de comprendre en profondeur plusieurs concepts fondamentaux de Docker :&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Image vs conteneur&lt;/strong&gt; : l'image est la recette figée ; le conteneur est l'instance qui tourne, vivante, modifiable, jetable. Cette distinction change la façon dont on pense l'architecture d'une application conteneurisée.&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Docker Compose comme outil d'orchestration&lt;/strong&gt; : en faisant communiquer mon "boxeur" et un "observateur" chargé de surveiller ses logs, j'ai compris comment Compose gère les dépendances entre services et le réseau interne qu'il crée automatiquement.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Les volumes et la persistance des données&lt;/strong&gt; : un bind mount simple, mais qui illustre bien pourquoi la persistance est un problème à part entière quand les conteneurs sont par nature éphémères.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Le poids des images comme critère d'architecture&lt;/strong&gt; : Alpine pèse environ 7 Mo contre une centaine de Mo pour une image basée sur Ubuntu. Comprendre cet écart m'a fait réaliser l'impact direct du choix d'image de base sur la vitesse de build et de déploiement.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Et puis, il y a eu ce moment précis :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker run &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;--name&lt;/span&gt; boxeur_mentor dreatech/boxeur:latest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Mon image, celle que j'avais mis du temps à comprendre et à construire, tournait enfin ; publiée, accessible, réelle.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Faga8862tpgz07lzophvx.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Faga8862tpgz07lzophvx.png" alt=" " width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Le rôle du mentorat
&lt;/h2&gt;

&lt;p&gt;Rien de tout ça ne serait arrivé aussi vite sans l'accompagnement d'&lt;a href="https://dev.to/bongoe"&gt;Endah&lt;/a&gt;, ma mentor dans le cadre du programme &lt;a href="https://www.eventbrite.com/e/cloudher-cohort-1-onboarding-launch-tickets-1992406228664" rel="noopener noreferrer"&gt;CloudHer&lt;/a&gt;, porté par Women Innovating In Cloud Africa (WIICA). Ses rappels réguliers m'ont poussée à sortir de la phase de lecture passive pour passer à la pratique ; pas en me donnant les réponses, mais en m'orientant vers les bonnes questions à me poser.&lt;/p&gt;

&lt;p&gt;Un bon mentorat, ce n'est pas qu'on fasse le travail à ta place. C'est qu'on te rappelle que tu es capable de comprendre le sujet toi-même, même quand tu doutes d'y arriver.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ce que je dirais à la moi d'il y a 30 jours
&lt;/h2&gt;

&lt;p&gt;Si tu débutes en DevOps et que Docker te semble être une montagne :&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;N'attends pas de tout comprendre en théorie avant de commencer.&lt;/strong&gt; Lance &lt;code&gt;docker run hello-world&lt;/code&gt;, puis va lire pourquoi ça fonctionne comme ça a fonctionné.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;L'erreur fait partie du chemin d'apprentissage.&lt;/strong&gt; Un 403, un &lt;code&gt;apk&lt;/code&gt; au lieu d'un &lt;code&gt;apt-get&lt;/code&gt;, un tag mal écrit — chaque erreur t'oblige à comprendre un mécanisme que tu aurais pu ignorer sinon.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Construis quelque chose pour comprendre un concept, pas pour produire un résultat.&lt;/strong&gt; Mon "boxeur" ne sert à rien de productif, mais c'est le projet qui m'a fait vraiment saisir ce que Docker fait et pourquoi.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Entoure-toi de quelqu'un qui challenge ta compréhension.&lt;/strong&gt; Le mentorat m'a poussée à expliquer ce que je faisais, pas juste à le faire.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Pour finir
&lt;/h2&gt;

&lt;p&gt;Mon image est disponible sur Docker Hub : &lt;code&gt;dreatech/boxeur&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fp7tjjiwbz6byji2wujou.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fp7tjjiwbz6byji2wujou.jpg" alt=" " width="720" height="1408"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Le premier &lt;code&gt;docker run&lt;/code&gt;, ce n'était pas difficile parce que la syntaxe est compliquée — mais parce qu'il faut accepter d'apprendre un système par la pratique, en tolérant de ne pas tout maîtriser dès le départ.&lt;/p&gt;

&lt;p&gt;Si toi aussi tu as un projet qui prend la poussière depuis des semaines parce que tu attends de tout comprendre avant de te lancer : prends le temps qu'il faut pour bien saisir les bases, puis lance la première commande.&lt;/p&gt;




&lt;p&gt;Tu es aussi en train d'apprendre Docker ou le DevOps ? Dis-moi en commentaire sur quoi tu bloques en ce moment;je serais ravie d'en discuter. Et si cet article t'a parlé, un partage aide énormément à faire connaître ce genre de retour d'expérience !&lt;/p&gt;

</description>
      <category>docker</category>
      <category>devops</category>
      <category>beginners</category>
      <category>motivation</category>
    </item>
    <item>
      <title>[Boost]</title>
      <dc:creator>Samuel Adekunle</dc:creator>
      <pubDate>Thu, 10 Sep 2026 22:27:33 +0000</pubDate>
      <link>https://dev.to/techwithsam/-ake</link>
      <guid>https://dev.to/techwithsam/-ake</guid>
      <description>&lt;div class="ltag__link--embedded"&gt;
  &lt;div class="crayons-story "&gt;
  &lt;a href="https://dev.to/techwithsam/automate-flutter-releases-with-shorebird-github-actions-skip-app-store-review-2j1l" class="crayons-story__hidden-navigation-link"&gt;Automate Flutter Releases with Shorebird + GitHub Actions (Skip App Store Review)&lt;/a&gt;
    &lt;div class="crayons-article__cover crayons-article__cover__image__feed"&gt;
      &lt;iframe src="https://www.youtube.com/embed/mHek8Uu32ow" title="Automate Flutter Releases with Shorebird + GitHub Actions (Skip App Store Review)"&gt;&lt;/iframe&gt;
    &lt;/div&gt;


  &lt;div class="crayons-story__body crayons-story__body-full_post"&gt;
    &lt;div class="crayons-story__top"&gt;
      &lt;div class="crayons-story__meta"&gt;
        &lt;div class="crayons-story__author-pic"&gt;

          &lt;a href="/techwithsam" class="crayons-avatar  crayons-avatar--l  "&gt;
            &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F283838%2Faf3610bc-683f-4e9d-8543-3f2117644325.jpg" alt="techwithsam profile" class="crayons-avatar__image"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
        &lt;div&gt;
          &lt;div&gt;
            &lt;a href="/techwithsam" class="crayons-story__secondary fw-medium m:hidden"&gt;
              Samuel Adekunle
            &lt;/a&gt;
            &lt;div class="profile-preview-card relative mb-4 s:mb-0 fw-medium hidden m:inline-block"&gt;
              
                Samuel Adekunle
                
                
              
              &lt;div id="story-author-preview-content-4627026" class="profile-preview-card__content crayons-dropdown branded-7 p-4 pt-0"&gt;
                &lt;div class="gap-4 grid"&gt;
                  &lt;div class="-mt-4"&gt;
                    &lt;a href="/techwithsam" class="flex"&gt;
                      &lt;span class="crayons-avatar crayons-avatar--xl mr-2 shrink-0"&gt;
                        &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F283838%2Faf3610bc-683f-4e9d-8543-3f2117644325.jpg" class="crayons-avatar__image" alt=""&gt;
                      &lt;/span&gt;
                      &lt;span class="crayons-link crayons-subtitle-2 mt-5"&gt;Samuel Adekunle&lt;/span&gt;
                    &lt;/a&gt;
                  &lt;/div&gt;
                  &lt;div class="print-hidden"&gt;
                    
                      Follow
                    
                  &lt;/div&gt;
                  &lt;div class="author-preview-metadata-container"&gt;&lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

          &lt;/div&gt;
          &lt;a href="https://dev.to/techwithsam/automate-flutter-releases-with-shorebird-github-actions-skip-app-store-review-2j1l" class="crayons-story__tertiary fs-xs"&gt;&lt;time&gt;Sep 10&lt;/time&gt;&lt;span class="time-ago-indicator-initial-placeholder"&gt;&lt;/span&gt;&lt;/a&gt;
        &lt;/div&gt;
      &lt;/div&gt;

    &lt;/div&gt;

    &lt;div class="crayons-story__indention"&gt;
      &lt;h2 class="crayons-story__title crayons-story__title-full_post"&gt;
        &lt;a href="https://dev.to/techwithsam/automate-flutter-releases-with-shorebird-github-actions-skip-app-store-review-2j1l" id="article-link-4627026"&gt;
          Automate Flutter Releases with Shorebird + GitHub Actions (Skip App Store Review)
        &lt;/a&gt;
      &lt;/h2&gt;
        &lt;div class="crayons-story__tags"&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/flutter"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;flutter&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/mobile"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;mobile&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/techwithsam"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;techwithsam&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/githubactions"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;githubactions&lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="crayons-story__bottom"&gt;
        &lt;div class="crayons-story__details"&gt;
          &lt;a href="https://dev.to/techwithsam/automate-flutter-releases-with-shorebird-github-actions-skip-app-store-review-2j1l" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left"&gt;
            &lt;div class="multiple_reactions_aggregate"&gt;
              &lt;span class="multiple_reactions_icons_container"&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/exploding-head-daceb38d627e6ae9b730f36a1e390fca556a4289d5a41abb2c35068ad3e2c4b5.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/multi-unicorn-b44d6f8c23cdd00964192bedc38af3e82463978aa611b4365bd33a0f1f4f3e97.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/sparkle-heart-5f9bee3767e18deb1bb725290cb151c25234768a0e9a2bd39370c382d02920cf.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
              &lt;/span&gt;
              &lt;span class="aggregate_reactions_counter"&gt;5&lt;span class="hidden s:inline"&gt;&amp;nbsp;reactions&lt;/span&gt;&lt;/span&gt;
            &lt;/div&gt;
          &lt;/a&gt;
            &lt;a href="https://dev.to/techwithsam/automate-flutter-releases-with-shorebird-github-actions-skip-app-store-review-2j1l#comments" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left flex items-center"&gt;
              

              &lt;span class="hidden s:inline"&gt;Add&amp;nbsp;Comment&lt;/span&gt;
            &lt;/a&gt;
        &lt;/div&gt;
        &lt;div class="crayons-story__save"&gt;
          &lt;small class="crayons-story__tertiary fs-xs mr-2"&gt;
            6 min read
          &lt;/small&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;/div&gt;


</description>
    </item>
    <item>
      <title>Automate Flutter Releases with Shorebird + GitHub Actions (Skip App Store Review)</title>
      <dc:creator>Samuel Adekunle</dc:creator>
      <pubDate>Thu, 10 Sep 2026 22:27:12 +0000</pubDate>
      <link>https://dev.to/techwithsam/automate-flutter-releases-with-shorebird-github-actions-skip-app-store-review-2j1l</link>
      <guid>https://dev.to/techwithsam/automate-flutter-releases-with-shorebird-github-actions-skip-app-store-review-2j1l</guid>
      <description>&lt;p&gt;A real pain point for mobile apps is waiting days or weeks for App Store and Play Store reviews whenever you need to ship a fix or a small update to users.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fswg5gbczn3brmqu7jeor.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fswg5gbczn3brmqu7jeor.png" alt="Elon Musk complaining about iOS App Review delay on X" width="800" height="366"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That exact problem is what we are solving in this article. I’ll show you exactly how to automate your Flutter releases and patches using Shorebird and GitHub Actions, so you can push updates to users in minutes instead of waiting for store review.&lt;/p&gt;

&lt;p&gt;We are going to create a clean, production-ready CI pipeline for both full releases and instant code-push updates.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;p&gt;Before we start, here’s what you need:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A &lt;a href="https://console.shorebird.dev/" rel="noopener noreferrer"&gt;Shorebird account&lt;/a&gt; and an app already set up with Shorebird&lt;/li&gt;
&lt;li&gt;Shorebird CLI installed and logged in on your machine&lt;/li&gt;
&lt;li&gt;A Flutter project that already has Shorebird initialized&lt;/li&gt;
&lt;li&gt;A GitHub repository&lt;/li&gt;
&lt;li&gt;Basic understanding of GitHub Actions&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you haven’t set up Shorebird in your Flutter app yet, check out &lt;a href="https://youtu.be/x7j3WnxfCc0" rel="noopener noreferrer"&gt;my video on YouTube on how to set up Shorebird&lt;/a&gt;. This article assumes that’s already done.&lt;/p&gt;

&lt;h2&gt;
  
  
  Authentication Setup
&lt;/h2&gt;

&lt;p&gt;The first thing we need is authentication so GitHub Actions can talk to Shorebird.&lt;/p&gt;

&lt;p&gt;Go to the &lt;a href="https://console.shorebird.dev/account" rel="noopener noreferrer"&gt;Shorebird Console&lt;/a&gt; → Account → API Keys → Create API Key.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fv9iplxigrfum3t692fd2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fv9iplxigrfum3t692fd2.png" alt="Shorebird API Key Interface" width="799" height="496"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Give it a clear name, e.g “GitHub Actions”, choose an expiration, and set the required permissions.&lt;/p&gt;

&lt;p&gt;Copy the key immediately because you won’t see it again.&lt;/p&gt;

&lt;p&gt;Now go to your GitHub repository → Settings → Secrets and variables → Actions → New repository secret.&lt;/p&gt;

&lt;p&gt;Name it exactly: &lt;code&gt;SHOREBIRD_TOKEN&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Paste the key and save.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9jctb6yd6tgrl0c8tuab.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9jctb6yd6tgrl0c8tuab.png" alt="GitHub Repository Secret Interface" width="800" height="509"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This token will be available in all your workflows as &lt;code&gt;${{ secrets.SHOREBIRD_TOKEN }}&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;That’s the only authentication step you need.&lt;/p&gt;

&lt;h2&gt;
  
  
  Official Shorebird GitHub Actions
&lt;/h2&gt;

&lt;p&gt;Shorebird provides three official GitHub Actions that make life much easier:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;shorebirdtech/setup-shorebird@v1 — installs Shorebird on the runner&lt;/li&gt;
&lt;li&gt;shorebirdtech/shorebird-release@v1 — creates a release&lt;/li&gt;
&lt;li&gt;shorebirdtech/shorebird-patch@v1 — creates a patch&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I strongly recommend using these instead of calling the CLI manually. They’re cleaner and maintained by the Shorebird team.&lt;/p&gt;

&lt;h2&gt;
  
  
  Release Workflow
&lt;/h2&gt;

&lt;p&gt;Let’s build the release workflow first.&lt;/p&gt;

&lt;p&gt;Create a new file in your project: &lt;code&gt;.github/workflows/shorebird-release.yml&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Here’s the structure:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Trigger on version tags: v1.0.0, v1.2.3, etc. or manually trigger the workflow&lt;/li&gt;
&lt;li&gt;Set the SHOREBIRD_TOKEN as an environment variable&lt;/li&gt;
&lt;li&gt;Pin your Flutter version (very important)&lt;/li&gt;
&lt;li&gt;Checkout the code&lt;/li&gt;
&lt;li&gt;Set up Java for Android (or Xcode signing for iOS)&lt;/li&gt;
&lt;li&gt;Set up Shorebird with caching&lt;/li&gt;
&lt;li&gt;Decode your keystore/certificates from secrets&lt;/li&gt;
&lt;li&gt;Run the official shorebird-release action&lt;/li&gt;
&lt;li&gt;Upload the APK, AAB, or IPA as artifacts&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I’ll show both Android and iOS versions.&lt;/p&gt;

&lt;p&gt;For Android, the key steps are decoding the keystore and creating the key.properties file from secrets.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;GitHub Secrets You Need to Add for Android&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Go to &lt;strong&gt;GitHub → repo → Settings → Secrets and variables → Actions → New repository secret&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;Secret name - Value
ANDROID_KEY_ALIAS - your_key_alias
ANDROID_KEY_PASSWORD - your_key_password
ANDROID_STORE_PASSWORD - your_store_password
ANDROID_KEYSTORE_BASE64 - run &lt;span class="sb"&gt;`&lt;/span&gt;&lt;span class="nb"&gt;base64&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; ~/path/to/upload.jks | &lt;span class="nb"&gt;tr&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'\n'&lt;/span&gt; | pbcopy&lt;span class="sb"&gt;`&lt;/span&gt;
The keystore &lt;span class="nb"&gt;base64 &lt;/span&gt;will be copied to your clipboard&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;paste &lt;/span&gt;it into GitHub Secrets
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;GitHub Secrets You Need to Add for iOS&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Go to &lt;strong&gt;GitHub → repo → Settings → Secrets and variables → Actions → New repository secret&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;Secret name - Value
IOS_CERTIFICATE_BASE64 - &lt;span class="nb"&gt;base64 &lt;/span&gt;of your .p12
IOS_CERTIFICATE_PASSWORD - Password you &lt;span class="nb"&gt;set &lt;/span&gt;when exporting the .p12
IOS_PROVISIONING_PROFILE_BASE64 - &lt;span class="nb"&gt;base64 &lt;/span&gt;of com.example.app profile
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For iOS, you need to import the certificate and provisioning profile into a temporary keychain.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# IOS_CERTIFICATE_BASE64 - export your .p12 from Keychain Access first, then:&lt;/span&gt;
&lt;span class="nb"&gt;base64&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; ~/path/to/distribution.p12 | &lt;span class="nb"&gt;tr&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'\n'&lt;/span&gt; | pbcopy

&lt;span class="c"&gt;# IOS_PROVISIONING_PROFILE_BASE64 - download from Apple Developer Portal&lt;/span&gt;
&lt;span class="nb"&gt;base64&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; ~/path/to/YourApp_AppStore.mobileprovision | &lt;span class="nb"&gt;tr&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'\n'&lt;/span&gt; | pbcopy
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can check out this document on how to create an &lt;a href="https://gist.github.com/techwithsam/e3ff26a6e12cde347e90158572e216c9#file-tutorial-md" rel="noopener noreferrer"&gt;Apple Distribution Certificate&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2rs4si20ng1ju8ez7xab.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2rs4si20ng1ju8ez7xab.png" alt="Repository secret" width="800" height="378"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Once this workflow finishes, you get signed artifacts ready to upload to the stores.&lt;/p&gt;

&lt;p&gt;This is your normal full release path.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Shorebird Release&lt;/span&gt;

&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;workflow_dispatch&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;inputs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;platform&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Target&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;platform'&lt;/span&gt;
        &lt;span class="na"&gt;required&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
        &lt;span class="na"&gt;default&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;both'&lt;/span&gt;
        &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;choice&lt;/span&gt;
        &lt;span class="na"&gt;options&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;android&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;ios&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;both&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;release_android&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Release Android&lt;/span&gt;
    &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ github.event.inputs.platform == 'android' || github.event.inputs.platform == 'both' }}&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Checkout Repository&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Setup Java&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-java@v4&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;distribution&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;temurin'&lt;/span&gt;
          &lt;span class="na"&gt;java-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;17'&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Setup Android Keystore&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;echo "${{ secrets.ANDROID_KEYSTORE_BASE64 }}" | base64 --decode &amp;gt; android/app/keystore.jks&lt;/span&gt;
          &lt;span class="s"&gt;cat &amp;gt; android/key.properties &amp;lt;&amp;lt;EOF&lt;/span&gt;
          &lt;span class="s"&gt;storePassword=${{ secrets.ANDROID_STORE_PASSWORD }}&lt;/span&gt;
          &lt;span class="s"&gt;keyPassword=${{ secrets.ANDROID_KEY_PASSWORD }}&lt;/span&gt;
          &lt;span class="s"&gt;keyAlias=${{ secrets.ANDROID_KEY_ALIAS }}&lt;/span&gt;
          &lt;span class="s"&gt;storeFile=keystore.jks&lt;/span&gt;
          &lt;span class="s"&gt;EOF&lt;/span&gt;
        &lt;span class="na"&gt;shell&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;bash&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Setup Shorebird&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;shorebirdtech/setup-shorebird@v1&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Shorebird Release (Android)&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;shorebirdtech/shorebird-release@v1&lt;/span&gt;
        &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;shorebird-release&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;flutter-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;latest&lt;/span&gt;
          &lt;span class="na"&gt;platform&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;android&lt;/span&gt;
        &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;SHOREBIRD_TOKEN&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.SHOREBIRD_TOKEN }}&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Print Release Version&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;echo "Released version ${{ steps.shorebird-release.outputs.release-version }}"&lt;/span&gt;
        &lt;span class="na"&gt;shell&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;bash&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Full release on &lt;a href="https://gist.github.com/techwithsam/e3ff26a6e12cde347e90158572e216c9#file-shorebird-release-yml" rel="noopener noreferrer"&gt;GitHub Gist&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Patch Workflow — The Real Power
&lt;/h2&gt;

&lt;p&gt;Now, the part that actually lets you skip App Store review.&lt;/p&gt;

&lt;p&gt;Create another file: &lt;code&gt;.github/workflows/shorebird-patch.yml&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Shorebird Patch&lt;/span&gt;

&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;branches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;main&lt;/span&gt;
  &lt;span class="na"&gt;workflow_dispatch&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;inputs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;platform&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Target&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;platform'&lt;/span&gt;
        &lt;span class="na"&gt;required&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
        &lt;span class="na"&gt;default&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;both'&lt;/span&gt;
        &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;choice&lt;/span&gt;
        &lt;span class="na"&gt;options&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;android&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;ios&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;both&lt;/span&gt;
      &lt;span class="na"&gt;release-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Release&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;version&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;to&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;patch&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;(e.g.&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;1.0.0+1).&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Defaults&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;to&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;latest.'&lt;/span&gt;
        &lt;span class="na"&gt;required&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
        &lt;span class="na"&gt;default&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;latest'&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;patch_android&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Patch Android&lt;/span&gt;
    &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ github.event_name == 'push' || github.event.inputs.platform == 'android' || github.event.inputs.platform == 'both' }}&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Checkout Repository&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Setup Java&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-java@v4&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;distribution&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;temurin'&lt;/span&gt;
          &lt;span class="na"&gt;java-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;17'&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Setup Android Keystore&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;echo "${{ secrets.ANDROID_KEYSTORE_BASE64 }}" | base64 --decode &amp;gt; android/app/keystore.jks&lt;/span&gt;
          &lt;span class="s"&gt;cat &amp;gt; android/key.properties &amp;lt;&amp;lt;EOF&lt;/span&gt;
          &lt;span class="s"&gt;storePassword=${{ secrets.ANDROID_STORE_PASSWORD }}&lt;/span&gt;
          &lt;span class="s"&gt;keyPassword=${{ secrets.ANDROID_KEY_PASSWORD }}&lt;/span&gt;
          &lt;span class="s"&gt;keyAlias=${{ secrets.ANDROID_KEY_ALIAS }}&lt;/span&gt;
          &lt;span class="s"&gt;storeFile=keystore.jks&lt;/span&gt;
          &lt;span class="s"&gt;EOF&lt;/span&gt;
        &lt;span class="na"&gt;shell&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;bash&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Setup Shorebird&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;shorebirdtech/setup-shorebird@v1&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Shorebird Patch (Android)&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;shorebirdtech/shorebird-patch@v1&lt;/span&gt;
        &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;shorebird-patch&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;platform&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;android&lt;/span&gt;
          &lt;span class="na"&gt;release-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ github.event.inputs.release-version || 'latest' }}&lt;/span&gt;
          &lt;span class="na"&gt;args&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;--allow-asset-diffs --allow-native-diffs&lt;/span&gt;
        &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;SHOREBIRD_TOKEN&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.SHOREBIRD_TOKEN }}&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Print Patch Number&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;echo "Patch number ${{ steps.shorebird-patch.outputs.patch-number }}"&lt;/span&gt;
        &lt;span class="na"&gt;shell&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;bash&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Full release on &lt;a href="https://gist.github.com/techwithsam/e3ff26a6e12cde347e90158572e216c9#file-shorebird-patch-yml" rel="noopener noreferrer"&gt;GitHub Gist&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Trigger this one on hotfix tags, for example: v1.0.0-hotfix.1&lt;/p&gt;

&lt;p&gt;The structure is almost identical to the release workflow, but we use the shorebird-patch action instead.&lt;/p&gt;

&lt;p&gt;Important points:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A patch can only be applied to an existing release&lt;/li&gt;
&lt;li&gt;Use the — staging flag first so you can preview the update&lt;/li&gt;
&lt;li&gt;After testing with shorebird preview — track=staging, promote it to production from the Shorebird Console.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is the real magic. Users get the fix over-the-air (OTA) in minutes. No waiting for review. No forcing them to download a new version from the store.&lt;/p&gt;

&lt;h2&gt;
  
  
  Recommended Development Workflow
&lt;/h2&gt;

&lt;p&gt;Here’s the clean workflow I recommend (and the one Shorebird documents):&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Work on features and fixes on feature branches&lt;/li&gt;
&lt;li&gt;Open a pull request into main&lt;/li&gt;
&lt;li&gt;CI runs formatting, analysis, and tests&lt;/li&gt;
&lt;li&gt;Squash and merge into main (main stays always releasable)&lt;/li&gt;
&lt;li&gt;When you’re ready for a full release → create a tag like v1.2.0 → the release workflow runs automatically&lt;/li&gt;
&lt;li&gt;If a critical bug appears → fix it on main → cherry-pick into the release branch → create a hotfix tag like v1.2.0-hotfix.1 → the patch workflow runs&lt;/li&gt;
&lt;li&gt;Preview on staging → promote to production&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This keeps everything automated, clean, and low-risk.&lt;/p&gt;

&lt;h2&gt;
  
  
  Best Practices
&lt;/h2&gt;

&lt;p&gt;A few important tips:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Always pin the Flutter version on releases using — flutter-version&lt;/li&gt;
&lt;li&gt;Use — dry-run on pull requests so you catch build issues early without creating real releases or patches&lt;/li&gt;
&lt;li&gt;Keep your SHOREBIRD_TOKEN secure — never hardcode it&lt;/li&gt;
&lt;li&gt;For iOS environments without certificates, you can use — no-codesign (then sign later)&lt;/li&gt;
&lt;li&gt;Shorebird automatically runs in non-interactive mode in CI when the token is present&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Follow these, and your pipeline will be solid.&lt;/p&gt;

&lt;h2&gt;
  
  
  Resources
&lt;/h2&gt;

&lt;p&gt;All of this is based on the official Shorebird documentation:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.shorebird.dev/code-push/ci/github/" rel="noopener noreferrer"&gt;GitHub Integration guide&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.shorebird.dev/code-push/ci/generic/" rel="noopener noreferrer"&gt;Generic CI guide&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.shorebird.dev/code-push/guides/development-workflow/" rel="noopener noreferrer"&gt;Development Workflow guide&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I’m also a &lt;a href="https://shorebird.dev/blog/meet-our-first-shorebird-community-ambassadors#:~:text=Samuel%20Adekunle%20(Nigeria)" rel="noopener noreferrer"&gt;Shorebird Ambassador&lt;/a&gt;, so if you have questions, drop them in the comments.&lt;/p&gt;




&lt;p&gt;That’s it.&lt;/p&gt;

&lt;p&gt;You now have a complete automated system:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Full releases go through the stores as usual&lt;/li&gt;
&lt;li&gt;Critical updates and bug fixes go live to users in minutes via Shorebird&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;No more waiting for review every single time.&lt;/p&gt;

&lt;p&gt;If this was useful, hit the like button, subscribe, and turn on notifications so you don’t miss the next post.&lt;/p&gt;

&lt;p&gt;If you want personalized help setting this up for your project, you can send a DM.&lt;/p&gt;

&lt;p&gt;I’ll see you in the next one. Peace.&lt;/p&gt;

</description>
      <category>flutter</category>
      <category>mobile</category>
      <category>techwithsam</category>
      <category>githubactions</category>
    </item>
    <item>
      <title>NBA play-in odds are not playoff odds: simulating the play-in tournament instead of guessing it</title>
      <dc:creator>Elio Liberatore</dc:creator>
      <pubDate>Thu, 10 Sep 2026 22:26:19 +0000</pubDate>
      <link>https://dev.to/commodus67/nba-play-in-odds-are-not-playoff-odds-simulating-the-play-in-tournament-instead-of-guessing-it-1kae</link>
      <guid>https://dev.to/commodus67/nba-play-in-odds-are-not-playoff-odds-simulating-the-play-in-tournament-instead-of-guessing-it-1kae</guid>
      <description>&lt;p&gt;In baseball, American football and hockey, "make the playoffs" is one line: finish above it and you're in. When I built an NBA version of my playoff-odds simulator, I assumed the same, and my first plan was to compare the model against the wrong market.&lt;/p&gt;

&lt;p&gt;Basketball has a band in the middle, and getting it right changes almost every number.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three zones, not two
&lt;/h2&gt;

&lt;p&gt;In each 15-team conference:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Finish&lt;/th&gt;
&lt;th&gt;What it means&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1st – 6th&lt;/td&gt;
&lt;td&gt;Straight into the playoffs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7th – 10th&lt;/td&gt;
&lt;td&gt;Into the &lt;strong&gt;play-in tournament&lt;/strong&gt;, where two of four survive&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;11th – 15th&lt;/td&gt;
&lt;td&gt;Season over&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The play-in works like this. Seed 7 plays seed 8; the winner is the 7th seed. Seed 9 plays seed 10; the loser goes home. The loser of 7 v 8 then hosts the winner of 9 v 10, and that game decides the 8th seed.&lt;/p&gt;

&lt;p&gt;Kalshi lists these as separate markets — one for playoff qualification across all 30 teams, and one play-in market per conference — and its contract rules settle the question in one sentence: "Qualifying for the play-in tournament doesn't constitute playoff qualification."&lt;/p&gt;

&lt;p&gt;So the play-in market is not a weaker version of the playoff market. It's a band, and the two behave almost like opposites. A title contender is a near-certainty for the playoffs and a near-zero for the play-in. A 44-win team can be close to a coin flip between the two.&lt;/p&gt;

&lt;h2&gt;
  
  
  Simulate the games, don't approximate them
&lt;/h2&gt;

&lt;p&gt;The tempting shortcut is to say "finish 7th or 8th, you're probably in; 9th or 10th, probably not". That breaks the moment you need two numbers that agree with each other: the chance of reaching the playoffs &lt;em&gt;and&lt;/em&gt; the chance of landing in the play-in.&lt;/p&gt;

&lt;p&gt;So in every simulated season the model:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Plays out the remaining regular-season schedule.&lt;/li&gt;
&lt;li&gt;Seeds each conference.&lt;/li&gt;
&lt;li&gt;Plays the three play-in games.&lt;/li&gt;
&lt;li&gt;Runs the full bracket: four rounds of best-of-seven series with the 2-2-1-1-1 home-court pattern.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That produces, for every team and from the same simulated seasons: top-six probability, play-in probability, playoff probability, conference finals, conference title and championship. Sanity checks come for free — the championship column sums to 1 across the league and conference finals to 4.&lt;/p&gt;

&lt;h2&gt;
  
  
  Basketball-specific modelling choices
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Pythagorean exponent.&lt;/strong&gt; Points scored and allowed predict future results better than record alone, but the exponent depends on the sport. Baseball uses about 1.83, hockey 2.0; for basketball I use 13.91. Plugging a baseball exponent into NBA scoring wildly compresses the gap between good and bad teams.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Uncertainty about strength itself.&lt;/strong&gt; My first version drew game outcomes from fixed team ratings. In September it confidently told me some teams made the playoffs 100% of the time and others 0%. That's not how the NBA works. Now each simulated season redraws every team's rating once (a 0.22 spread by default), so the output is a distribution rather than one confident guess. That alone cut the average gap against the market from 15.2 to 12.9 points and removed every 0% and 100%.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The two missing games.&lt;/strong&gt; The 2026-27 regular season is 82 games, but ESPN's schedule currently lists 80 per team: the other two depend on the NBA Cup and are scheduled later. If you simply simulate the schedule you can see, every team's projected wins sit on an 80-game scale. I simulate the missing two against an average opponent on a neutral floor.&lt;/p&gt;

&lt;h2&gt;
  
  
  A September snapshot
&lt;/h2&gt;

&lt;p&gt;From one run on 10 September 2026:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Team&lt;/th&gt;
&lt;th&gt;Projected wins&lt;/th&gt;
&lt;th&gt;Top six&lt;/th&gt;
&lt;th&gt;Play-in&lt;/th&gt;
&lt;th&gt;Playoffs&lt;/th&gt;
&lt;th&gt;Title&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Oklahoma City&lt;/td&gt;
&lt;td&gt;55.3&lt;/td&gt;
&lt;td&gt;97.0%&lt;/td&gt;
&lt;td&gt;2.8%&lt;/td&gt;
&lt;td&gt;99.1%&lt;/td&gt;
&lt;td&gt;23.9%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;San Antonio&lt;/td&gt;
&lt;td&gt;52.1&lt;/td&gt;
&lt;td&gt;91.6%&lt;/td&gt;
&lt;td&gt;7.8%&lt;/td&gt;
&lt;td&gt;97.0%&lt;/td&gt;
&lt;td&gt;11.8%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Boston&lt;/td&gt;
&lt;td&gt;51.6&lt;/td&gt;
&lt;td&gt;88.2%&lt;/td&gt;
&lt;td&gt;11.0%&lt;/td&gt;
&lt;td&gt;95.9%&lt;/td&gt;
&lt;td&gt;11.6%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Toronto&lt;/td&gt;
&lt;td&gt;44.8&lt;/td&gt;
&lt;td&gt;51.0%&lt;/td&gt;
&lt;td&gt;41.0%&lt;/td&gt;
&lt;td&gt;73.7%&lt;/td&gt;
&lt;td&gt;2.2%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Atlanta&lt;/td&gt;
&lt;td&gt;44.4&lt;/td&gt;
&lt;td&gt;48.2%&lt;/td&gt;
&lt;td&gt;42.4%&lt;/td&gt;
&lt;td&gt;71.7%&lt;/td&gt;
&lt;td&gt;1.8%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Miami&lt;/td&gt;
&lt;td&gt;44.3&lt;/td&gt;
&lt;td&gt;47.9%&lt;/td&gt;
&lt;td&gt;42.6%&lt;/td&gt;
&lt;td&gt;71.3%&lt;/td&gt;
&lt;td&gt;1.8%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Look at Toronto, Atlanta and Miami: roughly a coin flip to avoid the play-in, and more than 40% to end up in it. For those teams, playoff odds and play-in odds are both "live" — exactly why they need separate columns.&lt;/p&gt;

&lt;h2&gt;
  
  
  The number I don't trust
&lt;/h2&gt;

&lt;p&gt;Against Kalshi's playoff contracts that day, most of the table lines up within a few points: Oklahoma City 99.1% in the model against 98% on the market, Miami 71.3% against 72.5%, Toronto 73.7% against 70.5%.&lt;/p&gt;

&lt;p&gt;Then there's Charlotte: 86.5% in the model, 36% on the market. A fifty-point gap.&lt;/p&gt;

&lt;p&gt;That is not an opportunity. Before opening night, the model knows only how last season ended, regressed towards average. It hasn't seen free agency, the draft, trades or injuries. The market has. When I first measured the model against Kalshi in September, the rank correlation was about 0.80 and the average gap about 13 points — and the largest gaps were teams whose whole case this year is the offseason.&lt;/p&gt;

&lt;p&gt;So the simulator refuses to call anything "value" until every team has played ten games. It still reports every gap, labelled WATCH. A model that disagrees with the market by fifty points in September is telling you what it can't see, not what the market got wrong.&lt;/p&gt;

&lt;h2&gt;
  
  
  One market-side detail worth copying
&lt;/h2&gt;

&lt;p&gt;Playoff qualification is sixteen independent yes/no contracts, so prices across the league add up to about 16, not 1. Never normalise that field to 1. And price both sides of each contract: if the model says 60% and YES trades at 75 cents, the signal is on the NO side, not "no signal".&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;The simulator is published as an Apify Actor: &lt;a href="https://apify.com/commodus67/nba-playoff-odds-monte-carlo" rel="noopener noreferrer"&gt;NBA Playoff Odds API — Monte Carlo Simulator &amp;amp; Value Bets&lt;/a&gt;. Standings and schedules come from ESPN and prices from Kalshi's public API, with no keys needed. Ready-made examples include &lt;a href="https://apify.com/commodus67/nba-playoff-odds-monte-carlo/examples/nba-play-in-tournament-odds-by-conference" rel="noopener noreferrer"&gt;play-in tournament odds by conference&lt;/a&gt;, &lt;a href="https://apify.com/commodus67/nba-playoff-odds-monte-carlo/examples/nba-championship-odds-simulated-playoff-bracket" rel="noopener noreferrer"&gt;championship odds from a simulated bracket&lt;/a&gt;, &lt;a href="https://apify.com/commodus67/nba-playoff-odds-monte-carlo/examples/nba-projected-win-totals-for-all-30-teams" rel="noopener noreferrer"&gt;projected win totals&lt;/a&gt; and &lt;a href="https://apify.com/commodus67/nba-playoff-odds-monte-carlo/examples/track-nba-playoff-odds-all-season" rel="noopener noreferrer"&gt;tracking playoff odds all season&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The season starts on 20 October. Simulation and data, not tips.&lt;/p&gt;

</description>
      <category>datascience</category>
      <category>statistics</category>
      <category>sports</category>
      <category>simulation</category>
    </item>
    <item>
      <title>NHL playoff odds for 2026-27: 84 games, the wild card rule, and why I don't trust my own model in September</title>
      <dc:creator>Elio Liberatore</dc:creator>
      <pubDate>Thu, 10 Sep 2026 22:26:00 +0000</pubDate>
      <link>https://dev.to/commodus67/nhl-playoff-odds-for-2026-27-84-games-the-wild-card-rule-and-why-i-dont-trust-my-own-model-in-10hm</link>
      <guid>https://dev.to/commodus67/nhl-playoff-odds-for-2026-27-84-games-the-wild-card-rule-and-why-i-dont-trust-my-own-model-in-10hm</guid>
      <description>&lt;p&gt;I build Monte Carlo simulators that turn standings and schedules into playoff probabilities. After baseball, American football and soccer, I assumed hockey would be a copy-paste job with new team names.&lt;/p&gt;

&lt;p&gt;It wasn't. Four things about the NHL break a generic season simulator, and one of them changed this summer. Here they are, followed by the part I find most interesting: what the model says a few weeks before opening night, and why most of its disagreements with the market should be ignored.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Standings run on points, not wins
&lt;/h2&gt;

&lt;p&gt;A win is two points. An overtime or shootout loss is still worth one. Roughly 23% of NHL games go past regulation, so ranking simulated teams by wins misprices every club that lives in one-goal games.&lt;/p&gt;

&lt;p&gt;In the simulator each game has three outcomes — regulation win, overtime or shootout win, and the mirror images — and points are awarded the way the league awards them.&lt;/p&gt;

&lt;p&gt;There is a trap on the strength side too. Points percentage averages about .557 across the league because of the loser point, while win/loss averages exactly .500 because every game has a winner. If you estimate team strength from points percentage, every team looks slightly better than average. I estimate it from wins over games played, and from goals for and against with a Pythagorean exponent of 2.0.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. The playoff field is not "the top eight"
&lt;/h2&gt;

&lt;p&gt;Sixteen of 32 teams qualify, but not by conference rank. In each conference the top three of each division get in, and then the two best remaining teams take the wild cards regardless of division. A fourth-place team in a strong division and a third-place team in a weak one are not interchangeable.&lt;/p&gt;

&lt;p&gt;That rule has to run inside every simulated season. It also gives you a free sanity check: across all 32 teams, playoff probabilities must sum to exactly 16. If yours sum to 15.7, something is wrong.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Overtime is closer to a coin flip
&lt;/h2&gt;

&lt;p&gt;Three-on-three and the shootout are not sixty minutes of five-on-five hockey. A better team carries less of its edge into the extra period. I damp the strength gap by half in overtime (&lt;code&gt;overtimeDamping = 0.5&lt;/code&gt;). Setting it to 0 makes overtime a pure coin flip; 1 treats it like regulation.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. The season is 84 games now
&lt;/h2&gt;

&lt;p&gt;The collective bargaining agreement signed in 2025 moved the NHL to 84 regular-season games from 2026-27. ESPN's event count per team shows 84 for this season and 82 for last. I never hardcoded the season length — the simulator counts the games on the schedule feed — so projected point totals landed on the right scale without a code change. If you maintain a model with &lt;code&gt;82&lt;/code&gt; somewhere in it, now is the time to look.&lt;/p&gt;

&lt;p&gt;One more data quirk: ESPN names a season by the year it ends, so 2026-27 is &lt;code&gt;season=2027&lt;/code&gt;, and before opening night the 2027 standings tree exists but has zero teams in it. You need to fall back to the previous season for the list of clubs and divisions.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the model says right now
&lt;/h2&gt;

&lt;p&gt;Here is part of a 20,000-season run from 10 September 2026, next to the live price of Kalshi's "make the playoffs" contract for each team:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Team&lt;/th&gt;
&lt;th&gt;Projected points&lt;/th&gt;
&lt;th&gt;Model playoff %&lt;/th&gt;
&lt;th&gt;Kalshi&lt;/th&gt;
&lt;th&gt;Gap&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Colorado&lt;/td&gt;
&lt;td&gt;109.6&lt;/td&gt;
&lt;td&gt;96.5%&lt;/td&gt;
&lt;td&gt;91%&lt;/td&gt;
&lt;td&gt;+5.5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Carolina&lt;/td&gt;
&lt;td&gt;103.5&lt;/td&gt;
&lt;td&gt;82.9%&lt;/td&gt;
&lt;td&gt;89.5%&lt;/td&gt;
&lt;td&gt;−6.6&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Buffalo&lt;/td&gt;
&lt;td&gt;101.6&lt;/td&gt;
&lt;td&gt;76.3%&lt;/td&gt;
&lt;td&gt;56%&lt;/td&gt;
&lt;td&gt;+20.3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Edmonton&lt;/td&gt;
&lt;td&gt;95.7&lt;/td&gt;
&lt;td&gt;68.3%&lt;/td&gt;
&lt;td&gt;85%&lt;/td&gt;
&lt;td&gt;−16.7&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Vegas&lt;/td&gt;
&lt;td&gt;95.2&lt;/td&gt;
&lt;td&gt;66.4%&lt;/td&gt;
&lt;td&gt;85.5%&lt;/td&gt;
&lt;td&gt;−19.1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Boston&lt;/td&gt;
&lt;td&gt;97.0&lt;/td&gt;
&lt;td&gt;58.3%&lt;/td&gt;
&lt;td&gt;31%&lt;/td&gt;
&lt;td&gt;+27.3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pittsburgh&lt;/td&gt;
&lt;td&gt;95.5&lt;/td&gt;
&lt;td&gt;52.5%&lt;/td&gt;
&lt;td&gt;29.5%&lt;/td&gt;
&lt;td&gt;+23.0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;San Jose&lt;/td&gt;
&lt;td&gt;89.7&lt;/td&gt;
&lt;td&gt;42.3%&lt;/td&gt;
&lt;td&gt;67.5%&lt;/td&gt;
&lt;td&gt;−25.2&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Boston 27 points above the market. San Jose 25 below. If you believed the model, those would be the trades of the year.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I don't believe it (yet)
&lt;/h2&gt;

&lt;p&gt;In September the model knows exactly one thing about each club: how last season ended, shrunk 40% towards average. It hasn't seen a trade, a signing, an injury, a goalie change or a new coach. The market has seen all of them.&lt;/p&gt;

&lt;p&gt;So the biggest gaps in September are not edges. &lt;strong&gt;They are the offseason.&lt;/strong&gt; Betting them is betting that the summer didn't happen.&lt;/p&gt;

&lt;p&gt;When I first compared the model with Kalshi across all 32 teams, the rank correlation was 0.65 and the average gap was about 14 points, and the largest disagreements were precisely the teams whose summers changed the most. That is what a season-carryover model should look like before the puck drops.&lt;/p&gt;

&lt;p&gt;Instead of hiding that, I made it a rule in the output. Until every team has played a minimum number of games (10 by default), no row is allowed to call itself value. Every gap is still reported in full, but it's labelled WATCH. After that, the current season's record takes over from last season's gradually, rather than overnight after a hot week.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fee changes which gaps matter
&lt;/h2&gt;

&lt;p&gt;Kalshi's taker fee is 0.07 × p × (1 − p) per contract. It peaks in the middle: 1.75 cents on a 50-cent contract, 0.63 cents on a 90-cent one. So a 1.5-point edge on a coin-flip contract is a losing position after fees, while the same gap on a heavy favourite isn't. Any comparison that ignores this will rank the wrong teams first.&lt;/p&gt;

&lt;p&gt;A second trap: the playoff market is sixteen independent yes/no contracts, so prices across the league add up to about 16, not 1. If you "de-vig" it the way you would a division-winner market, you divide every probability by sixteen and manufacture huge fake edges everywhere. Division winners, on the other hand, are exclusive — exactly one team wins — and there you do strip the overround.&lt;/p&gt;

&lt;h2&gt;
  
  
  Watching it move
&lt;/h2&gt;

&lt;p&gt;The part I'm looking forward to is not the September table. It is the path. Each run can append its 32 rows to a named dataset that keeps growing, so a daily schedule gives you, by spring, how each team's probability moved across the season next to what the market charged for it on the same day — something you can't reconstruct afterwards.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;The simulator is published as an Apify Actor: &lt;a href="https://apify.com/commodus67/nhl-playoff-odds-monte-carlo" rel="noopener noreferrer"&gt;NHL Playoff Odds API — Monte Carlo Simulator &amp;amp; Value Bets&lt;/a&gt;. No API keys for the data; standings and schedules come from ESPN, prices from Kalshi's public API. There are ready-made examples for &lt;a href="https://apify.com/commodus67/nhl-playoff-odds-monte-carlo/examples/nhl-projected-points-standings-all-32-teams" rel="noopener noreferrer"&gt;projected points standings&lt;/a&gt;, &lt;a href="https://apify.com/commodus67/nhl-playoff-odds-monte-carlo/examples/nhl-wild-card-odds-all-32-teams" rel="noopener noreferrer"&gt;wild card odds&lt;/a&gt;, &lt;a href="https://apify.com/commodus67/nhl-playoff-odds-monte-carlo/examples/nhl-presidents-trophy-odds" rel="noopener noreferrer"&gt;Presidents' Trophy odds&lt;/a&gt; and &lt;a href="https://apify.com/commodus67/nhl-playoff-odds-monte-carlo/examples/track-nhl-playoff-odds-all-season" rel="noopener noreferrer"&gt;tracking playoff odds all season&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Simulation and data, not tips.&lt;/p&gt;

</description>
      <category>datascience</category>
      <category>statistics</category>
      <category>sports</category>
      <category>simulation</category>
    </item>
    <item>
      <title>Correct score, BTTS and Over/Under probabilities with Dixon-Coles: what I learned building it for MLS and Liga MX</title>
      <dc:creator>Elio Liberatore</dc:creator>
      <pubDate>Thu, 10 Sep 2026 22:25:35 +0000</pubDate>
      <link>https://dev.to/commodus67/correct-score-btts-and-overunder-probabilities-with-dixon-coles-what-i-learned-building-it-for-39dc</link>
      <guid>https://dev.to/commodus67/correct-score-btts-and-overunder-probabilities-with-dixon-coles-what-i-learned-building-it-for-39dc</guid>
      <description>&lt;p&gt;Most football prediction pages give you three numbers — home, draw, away — and no way to check where they came from. I wanted the opposite: one model, fitted on real results, that produces the 1X2 probabilities, the Over/Under 2.5 line, Both Teams To Score and the exact-score grid, all from the same place, so they can't contradict each other.&lt;/p&gt;

&lt;p&gt;The model I ended up with is Dixon-Coles. This post is what it does, why it beats the simpler version most tutorials start with, and what came out when I ran it on eight leagues that don't get much attention from modellers: MLS, Liga MX, Liga de Expansión MX, the Brasileirão Série B, the USL Championship, Colombia's Primera A, Uruguay's Primera División and Norway's Eliteserien.&lt;/p&gt;

&lt;h2&gt;
  
  
  The starting point: independent Poisson
&lt;/h2&gt;

&lt;p&gt;The classic approach gives every team an attack rating and a defence rating, adds a home advantage, and turns them into expected goals for each side of a fixture — call them λ for the home team and μ for the away team. Goals are then treated as two independent Poisson variables. The probability of a 2-1 is just P(home scores 2) × P(away scores 1).&lt;/p&gt;

&lt;p&gt;It works surprisingly well. It also has one known blind spot: &lt;strong&gt;low scores&lt;/strong&gt;. Real matches finish 0-0 and 1-1 more often than two independent Poisson draws predict, and 1-0 / 0-1 slightly less often. Anything that depends on those four cells — the draw, Under 2.5, BTTS "No" — inherits the error.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Dixon-Coles correction
&lt;/h2&gt;

&lt;p&gt;In 1997 Mark Dixon and Stuart Coles proposed a small fix. Keep the Poisson grid, but multiply the four low-score cells by a factor that depends on one extra parameter, ρ (rho):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Score&lt;/th&gt;
&lt;th&gt;Adjustment&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;0-0&lt;/td&gt;
&lt;td&gt;1 − λμρ&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;0-1&lt;/td&gt;
&lt;td&gt;1 + λρ&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1-0&lt;/td&gt;
&lt;td&gt;1 + μρ&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1-1&lt;/td&gt;
&lt;td&gt;1 − ρ&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;anything else&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;With a negative ρ, 0-0 and 1-1 go up and 1-0 / 0-1 go down — exactly the direction the data pulls.&lt;/p&gt;

&lt;p&gt;To see how much that matters, take a match with 1.35 expected goals for the home side and 1.15 for the away side, and an illustrative ρ of −0.13:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Independent Poisson&lt;/th&gt;
&lt;th&gt;Dixon-Coles&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Draw&lt;/td&gt;
&lt;td&gt;26.8%&lt;/td&gt;
&lt;td&gt;30.2%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;0-0&lt;/td&gt;
&lt;td&gt;8.2%&lt;/td&gt;
&lt;td&gt;9.9%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1-1&lt;/td&gt;
&lt;td&gt;12.7%&lt;/td&gt;
&lt;td&gt;14.4%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Home win&lt;/td&gt;
&lt;td&gt;41.3%&lt;/td&gt;
&lt;td&gt;39.7%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Away win&lt;/td&gt;
&lt;td&gt;31.8%&lt;/td&gt;
&lt;td&gt;30.2%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Same expected goals, three and a half points more on the draw. If you compare model probabilities with prices, that is the difference between seeing an edge and not seeing one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two things the paper adds that tutorials often skip
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Time decay.&lt;/strong&gt; A result from three seasons ago should not count as much as last weekend's. Dixon and Coles weight each match by exp(−ξ·t), where t is its age in days. I use ξ = 0.0018, which halves a result's weight after roughly a year, and fit on three seasons of history.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fit ρ from the league itself.&lt;/strong&gt; ρ is not a universal constant. After fitting attack, defence, home advantage and the baseline by weighted maximum likelihood, I fit ρ separately for each league. They come out different. On 6 September, Colombia's Primera A gave a home advantage of 0.349 (on the log scale) and ρ = −0.044; Norway's Eliteserien gave 0.276 and ρ = −0.015. Colombian home sides get a bigger boost, and the low-score correction matters less in Norway.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the data comes from
&lt;/h2&gt;

&lt;p&gt;All eight leagues come from ESPN's public scoreboard endpoint:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://site.api.espn.com/apis/site/v2/sports/soccer/&amp;lt;league&amp;gt;/scoreboard?dates=YYYYMMDD-YYYYMMDD
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two things cost me time and might save you some:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Ask for a date range, not a single day.&lt;/strong&gt; With &lt;code&gt;dates=&lt;/code&gt; set to one day, a smaller league often returns nothing simply because it didn't play that day, which looks exactly like missing data. A week-long range removes the ambiguity.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Filter by state, not by status name.&lt;/strong&gt; Finished matches carry &lt;code&gt;status.type.state === "post"&lt;/code&gt; and &lt;code&gt;completed: true&lt;/code&gt;; scheduled ones are &lt;code&gt;"pre"&lt;/code&gt;. That is more robust than matching &lt;code&gt;STATUS_FULL_TIME&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Slugs are not always what you'd guess. The USL Championship is &lt;code&gt;usa.usl.1&lt;/code&gt;, not &lt;code&gt;usa.2&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;History depth is not the bottleneck. The Premier League scoreboard answers back to at least 2002-03, and MLS and Liga MX back to at least 2004-05. Three seasons is plenty.&lt;/p&gt;

&lt;h2&gt;
  
  
  What one prediction looks like
&lt;/h2&gt;

&lt;p&gt;Here is a real row from a test run on 6 September — Atlanta United at home to Orlando City in MLS:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;λ home / λ away&lt;/td&gt;
&lt;td&gt;1.57 / 1.63&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ρ (MLS)&lt;/td&gt;
&lt;td&gt;−0.040&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Home / Draw / Away&lt;/td&gt;
&lt;td&gt;36.5% / 24.2% / 39.2%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Over 2.5 goals&lt;/td&gt;
&lt;td&gt;62.1%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Both teams to score&lt;/td&gt;
&lt;td&gt;64.1%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;And from Liga MX the same day, Pumas UNAM against León: λ 1.94 against 0.94, so 60.1% / 23.0% / 16.9%. A strong home side, and the numbers say so.&lt;/p&gt;

&lt;p&gt;Every probability comes from one score grid (0-0 up to 10-10), normalised to 1, so 1X2 sums to 1, Over + Under sums to 1 and BTTS Yes + No sums to 1. I checked that on every row of a 28-match Série B run; the error was floating-point noise.&lt;/p&gt;

&lt;h2&gt;
  
  
  The awkward case: promoted teams
&lt;/h2&gt;

&lt;p&gt;A team with no matches in the lookback window has no rating. The honest options are to guess or to say so. I start it at league-average strength and flag every fixture it plays with &lt;code&gt;dataQuality: "partial-new-team"&lt;/code&gt;, so whoever uses the numbers can decide how much to trust them until the team has a few games on the board.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it doesn't do
&lt;/h2&gt;

&lt;p&gt;It doesn't know about injuries, suspensions, rotation, weather or a manager who has just been sacked. It treats every match in the lookback window the same apart from its age. It is a baseline, not an oracle — which is exactly what makes it useful to compare against prices.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it without writing the model
&lt;/h2&gt;

&lt;p&gt;I packaged all of this as an Apify Actor: &lt;a href="https://apify.com/commodus67/soccer-dixon-coles-match-predictor" rel="noopener noreferrer"&gt;Soccer Match Predictions API — 1X2, Over/Under &amp;amp; BTTS Odds&lt;/a&gt;. You pick a league, it returns one row per upcoming fixture with everything above. There are ready-made examples, such as &lt;a href="https://apify.com/commodus67/soccer-dixon-coles-match-predictor/examples/mls-correct-score-probabilities" rel="noopener noreferrer"&gt;MLS correct score probabilities&lt;/a&gt; and &lt;a href="https://apify.com/commodus67/soccer-dixon-coles-match-predictor/examples/liga-mx-match-predictions-1x2-over-under-btts" rel="noopener noreferrer"&gt;Liga MX match predictions&lt;/a&gt;, and the rest are listed on the &lt;a href="https://apify.com/commodus67/soccer-dixon-coles-match-predictor/examples" rel="noopener noreferrer"&gt;examples page&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If you'd rather build it yourself, the table of adjustments above and the ESPN endpoint are all you need to get started. Either way: simulation and data, not tips.&lt;/p&gt;

</description>
      <category>datascience</category>
      <category>statistics</category>
      <category>sports</category>
      <category>api</category>
    </item>
    <item>
      <title>I Asked a Frontier LLM to Recover Secrets from My Decompiled Build</title>
      <dc:creator>Nikolai Sachok</dc:creator>
      <pubDate>Thu, 10 Sep 2026 22:18:06 +0000</pubDate>
      <link>https://dev.to/nsachok/i-asked-a-frontier-llm-to-recover-secrets-from-my-decompiled-build-1ojb</link>
      <guid>https://dev.to/nsachok/i-asked-a-frontier-llm-to-recover-secrets-from-my-decompiled-build-1ojb</guid>
      <description>&lt;p&gt;The clean way to protect application logic is to keep it on a server, out of reach. When it has to live in the client, anyone can pull apart and analyze the compiled app on their device. Obfuscation cannot make recovery impossible; it can only raise its cost. Making a secret unreadable in a decompiler leaves an awkward question: how hard is it to recover?&lt;/p&gt;

&lt;p&gt;That was the question I wanted to answer about my own build hardening. I could inspect the output and confirm that configuration values no longer appeared as readable strings. But I knew how the hardening worked. I knew which data mattered, where decoding happened, and what relationships to look for. My ability to recognize the result told me little about how difficult it would be to discover those relationships without that context.&lt;/p&gt;

&lt;p&gt;Out of engineering curiosity, I gave a frontier LLM the build and had it investigate. I wanted to see whether it could work backward from the shipped artifact to the secrets the application could recover for itself. The interesting part of the experiment became the investigation around the model: how to challenge its interpretations, what evidence to demand, and how much confidence to place in a failed recovery attempt.&lt;/p&gt;

&lt;h2&gt;
  
  
  The master key ships inside the binary
&lt;/h2&gt;

&lt;p&gt;I started with a boundary that had to remain explicit throughout: the master key ships inside the binary.&lt;/p&gt;

&lt;p&gt;The application needs to decode its values, so the artifact contains what it needs to do that. A sufficiently capable reverse engineer might find the master key, understand the derivation, and reproduce the decoding. I wrote that limitation into the threat model. The hardening aims to raise the cost of reverse engineering and make encoded values harder to associate with one another.&lt;/p&gt;

&lt;p&gt;I treated this as an obfuscation test. Calling it cryptographic protection would obscure the engineering question I could actually investigate: given the build, could an adversary find and use the material already inside it?&lt;/p&gt;

&lt;h2&gt;
  
  
  Making the relationships harder to discover
&lt;/h2&gt;

&lt;p&gt;The configuration values are XOR-encoded. Each value uses a working key derived at runtime from a single 32-byte master key. Those working keys are never stored at rest in the binary.&lt;/p&gt;

&lt;p&gt;XOR is straightforward to reverse once the corresponding key bytes are available. The difficulty I was trying to introduce therefore lay in discovery: recognizing which arrays held encoded values, finding the master key, and connecting it to the derivation and decoding logic. Removing readable strings eliminates an easy starting point, but those relationships are what an investigator ultimately needs to reconstruct.&lt;/p&gt;

&lt;p&gt;The encoded values appear as numeric byte-array literals resembling hashes. There is no string in the binary that reads as a key. To someone who already understands the implementation, these arrays have clear roles. To someone examining the build blind, their appearance supplies much less context.&lt;/p&gt;

&lt;p&gt;I also wanted to remove similarities between related values. Shared prefixes can give an investigator a useful foothold: several values that look alike may belong together, and understanding one can guide the investigation of the others. The derivation deliberately strips shared prefixes so that similar secrets do not produce similar-looking output.&lt;/p&gt;

&lt;p&gt;That matters because recovery does not have to begin with decoding. An investigator might first cluster values, infer that a group shares a purpose, and then search for the code consuming it. Decorrelation is intended to make that earlier step harder. It removes a recognizable relationship from the output, while leaving the application able to recover each value.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three implementations must agree byte for byte
&lt;/h2&gt;

&lt;p&gt;There is an implementation cost to this arrangement. The derivation exists in three independent places: the application runtime, a build-time tool, and a native build script. They are written in three different languages and must agree byte for byte.&lt;/p&gt;

&lt;p&gt;Conceptual agreement is insufficient here. Each implementation has to produce exactly the bytes the others expect. Differences in how languages handle bytes and numeric operations can matter when the output of one implementation becomes the input to another. If the derivations diverge, the application fails to decode its values.&lt;/p&gt;

&lt;p&gt;That requirement gave the hardening two distinct questions to answer. The implementations had to agree exactly for the application to function, and the resulting artifact had to be difficult to interpret without knowing the design. Agreement addresses correctness. A blind investigation addresses the second question.&lt;/p&gt;

&lt;h2&gt;
  
  
  An adversarial court
&lt;/h2&gt;

&lt;p&gt;For that investigation, I used a group of LLM agents organized as an adversarial court.&lt;/p&gt;

&lt;p&gt;Three neutral researchers gathered facts from different parts of the build. One examined code, permissions, and obfuscation patterns. Another focused on network and data. The third investigated libraries and behavior. Their job was to establish what the artifact supported before turning observations into an argument.&lt;/p&gt;

&lt;p&gt;A prosecutor then argued for findings, with a deliberate bias toward identifying problems. An advocate challenged those interpretations and supplied mitigating or benign explanations. An impartial judge weighed the record and decided which conclusions had enough support.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvcodrqezarmkxqwq8w06.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvcodrqezarmkxqwq8w06.png" alt="Conceptual adversarial court: three neutral researchers gather facts about code, permissions and obfuscation; network and data; and libraries and behavior. A prosecutor argues for findings, an advocate challenges interpretations, and an impartial judge weighs the evidence and both arguments." width="800" height="547"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The separation mattered because a single analysis prompt makes it easy for one interpretation to dominate. Once an explanation looks plausible, subsequent observations can be fitted around it. Asking the same analysis to find evidence, develop a theory, challenge the theory, and deliver a verdict gives it several responsibilities that pull in different directions.&lt;/p&gt;

&lt;p&gt;The opposed roles made those tensions explicit. The prosecutor had reason to pursue an uncomfortable interpretation. The advocate had reason to identify missing links and explain why the same observation might be harmless. The judge had a record containing both arguments.&lt;/p&gt;

&lt;p&gt;This arrangement does not make the agents independent sources of truth. They can still share blind spots or make the same mistake. Its practical value is that objections become part of the process, with a role responsible for developing them. A persuasive account has to survive a challenge before it becomes a finding.&lt;/p&gt;

&lt;h2&gt;
  
  
  What counts as independent evidence
&lt;/h2&gt;

&lt;p&gt;That only helps if the challenge is technical. I required two or three independent technical indicators before accepting a conclusion. A single suspicious pattern remained a lead.&lt;/p&gt;

&lt;p&gt;The word “independent” carries much of the weight. Several agents repeating the same observation do not create several pieces of evidence. Nor does describing one byte array in three different ways. Corroboration has to add support that the original observation did not already contain.&lt;/p&gt;

&lt;p&gt;For example, the presence of an encoded-looking array is a reason to investigate. A stronger case would connect that array to reachable decoding logic and then connect the decoded result to its use. Each connection answers a different question: what the data might be, whether the relevant code can run, and whether the interpretation fits the application’s behavior. That is the kind of reasoning the evidence requirement was intended to demand.&lt;/p&gt;

&lt;p&gt;Reachability was another explicit check. Code existing in a decompiled build does not by itself establish that the application executes it. An interpretation resting on a theoretical path deserves less confidence than one supported by observed behavior.&lt;/p&gt;

&lt;p&gt;I also required the investigation to consider a legitimate explanation before concluding. That gave the advocate a concrete task. It had to explain how the available evidence could fit an ordinary purpose, and identify what further evidence would distinguish the competing interpretations.&lt;/p&gt;

&lt;p&gt;These rules gave the judge a basis for weighing the record beyond which agent sounded most certain. An accusation could be plausible and still unsupported. A benign explanation could be possible and still fail to account for the evidence. The purpose of the process was to make those gaps visible.&lt;/p&gt;

&lt;p&gt;This is where role separation earns its overhead. A single broad analysis request can produce a fluent story whose weak points are difficult to see. Opposed roles and an evidence bar force more of the reasoning into view, helping surface supported findings and reducing room for invented explanations. That gives me a practical reason to prefer the structure, without treating this experiment as a measurement of its advantage across models or artifacts.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the blind attempt established
&lt;/h2&gt;

&lt;p&gt;In this blind attempt, the model could neither identify the master key nor cluster the encoded values. It had only the build and had not been told what to look for. The hardening held against that attempt.&lt;/p&gt;

&lt;p&gt;The qualification “blind” is essential. Pointing an investigator at a particular array, explaining the derivation, or identifying the decoding path changes the task. Discovery is part of the reverse-engineering cost, and this experiment included it. The model’s failure to discover the necessary relationships says something useful about that cost under the tested conditions, even though it does not quantify it.&lt;/p&gt;

&lt;p&gt;It does not establish that recovery is impossible. The master key remains in the binary. Another investigator, a different approach, or additional guidance could produce a different result. One unsuccessful attempt cannot settle those possibilities.&lt;/p&gt;

&lt;p&gt;What I gained was a concrete observation about the shipped artifact from an adversary that lacked my implementation knowledge. That was more useful than inspecting unreadable values and deciding they looked sufficiently obscure.&lt;/p&gt;

&lt;p&gt;LLMs make this kind of artifact testing practical and inexpensive enough to be worth doing. Their usefulness extends beyond reviewing source code: they can investigate what a build reveals, particularly when their conclusions have to pass through competing interpretations and explicit evidence requirements.&lt;/p&gt;

&lt;p&gt;For me, the strongest result was a bounded one: a frontier LLM operating blind failed to find the master key or cluster the encoded values, despite an investigation designed to challenge comfortable conclusions. I can use that result because I can state its limits just as plainly. The key is still there; this adversary did not find it.&lt;/p&gt;

</description>
      <category>security</category>
      <category>llm</category>
      <category>testing</category>
      <category>reverseengineering</category>
    </item>
    <item>
      <title>How I Slashed a Docker Image from 442 MB to 56 MB (87% Cut) and Hardened It for Production</title>
      <dc:creator>Alan Varghese</dc:creator>
      <pubDate>Thu, 10 Sep 2026 22:17:59 +0000</pubDate>
      <link>https://dev.to/alanvarghese-dev/how-i-slashed-a-docker-image-from-442-mb-to-56-mb-87-cut-and-hardened-it-for-production-1om4</link>
      <guid>https://dev.to/alanvarghese-dev/how-i-slashed-a-docker-image-from-442-mb-to-56-mb-87-cut-and-hardened-it-for-production-1om4</guid>
      <description>&lt;p&gt;We've all been there: you whip up a simple microservice in Python, write a quick Dockerfile, run &lt;code&gt;docker build&lt;/code&gt;, and suddenly your container image weighs almost &lt;strong&gt;half a gigabyte&lt;/strong&gt; (or &lt;strong&gt;1.75 GB&lt;/strong&gt; uncompressed on disk!).&lt;/p&gt;

&lt;p&gt;For a 15-line Flask application with two routes? That felt unacceptable.&lt;/p&gt;

&lt;p&gt;Bloated Docker images slow down CI/CD pipelines, increase registry storage bills, consume unnecessary bandwidth during deployment, and—worst of all—expand the security attack surface with packages that have no business being in a production container.&lt;/p&gt;

&lt;p&gt;In this project, I took a bloated baseline image and systematically redesigned it. The outcome?&lt;/p&gt;

&lt;p&gt;🔥 &lt;strong&gt;Image Content Size:&lt;/strong&gt; Reduced from &lt;strong&gt;442 MB&lt;/strong&gt; to &lt;strong&gt;56.6 MB&lt;/strong&gt; (&lt;strong&gt;87.19% reduction&lt;/strong&gt;)&lt;br&gt;&lt;br&gt;
💾 &lt;strong&gt;Disk Usage:&lt;/strong&gt; Dropped from &lt;strong&gt;1.75 GB&lt;/strong&gt; to &lt;strong&gt;256 MB&lt;/strong&gt; (&lt;strong&gt;85.37% reduction&lt;/strong&gt;)&lt;br&gt;&lt;br&gt;
🛡️ &lt;strong&gt;Security:&lt;/strong&gt; Transitioned from running as root to a hardened, non-root system user&lt;br&gt;&lt;br&gt;
⚡ &lt;strong&gt;Build Time:&lt;/strong&gt; Dramatically improved iterative build speeds using Docker layer caching&lt;br&gt;&lt;br&gt;
🚀 &lt;strong&gt;Runtime:&lt;/strong&gt; Replaced Flask's single-threaded dev server with a production WSGI server (Gunicorn)  &lt;/p&gt;

&lt;p&gt;Here is the full breakdown of how I did it, the pitfalls I ran into, and the key lessons you can apply to your own containers today.&lt;/p&gt;


&lt;h2&gt;
  
  
  🛑 The "Before": A Naive, Bloated Baseline
&lt;/h2&gt;

&lt;p&gt;Here is what our initial &lt;code&gt;Dockerfile.baseline&lt;/code&gt; looked like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; python:3.12&lt;/span&gt;

&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /app&lt;/span&gt;

&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; . .&lt;/span&gt;

&lt;span class="k"&gt;RUN &lt;/span&gt;apt-get update
&lt;span class="k"&gt;RUN &lt;/span&gt;apt-get &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; curl git vim

&lt;span class="k"&gt;RUN &lt;/span&gt;pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--no-cache-dir&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; requirements.txt

&lt;span class="k"&gt;EXPOSE&lt;/span&gt;&lt;span class="s"&gt; 5000&lt;/span&gt;

&lt;span class="k"&gt;CMD&lt;/span&gt;&lt;span class="s"&gt; ["python", "app.py"]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At first glance, it looks familiar. It sets a workdir, copies files, installs tools, installs dependencies, and runs the app.&lt;/p&gt;

&lt;p&gt;Let's build it and inspect the damage:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker build &lt;span class="nt"&gt;-f&lt;/span&gt; Dockerfile.baseline &lt;span class="nt"&gt;-t&lt;/span&gt; myapp:baseline &lt;span class="nb"&gt;.&lt;/span&gt;
docker images myapp:baseline
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;REPOSITORY   TAG        IMAGE ID       CREATED          SIZE
myapp        baseline   a1b2c3d4e5f6   10 seconds ago   442MB
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Checking uncompressed disk usage with Docker desktop / inspect: &lt;strong&gt;1.75 GB&lt;/strong&gt;!&lt;/p&gt;

&lt;h3&gt;
  
  
  What Went Wrong Here?
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The Base Image:&lt;/strong&gt; &lt;code&gt;python:3.12&lt;/code&gt; is built on a full Debian distribution packed with compilers, header files, and utilities our web app will never call.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Unnecessary Packages:&lt;/strong&gt; We installed &lt;code&gt;curl&lt;/code&gt;, &lt;code&gt;git&lt;/code&gt;, and &lt;code&gt;vim&lt;/code&gt;. Why does a production container need a text editor and a version control tool?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Separate &lt;code&gt;RUN&lt;/code&gt; instructions:&lt;/strong&gt; &lt;code&gt;RUN apt-get update&lt;/code&gt; and &lt;code&gt;RUN apt-get install&lt;/code&gt; created two separate filesystem layers, storing temporary cache files forever in the image layer history.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Poor Layer Caching:&lt;/strong&gt; &lt;code&gt;COPY . .&lt;/code&gt; came &lt;em&gt;before&lt;/em&gt; &lt;code&gt;RUN pip install&lt;/code&gt;. Any tiny change to &lt;code&gt;app.py&lt;/code&gt; busted Docker's cache and forced &lt;code&gt;pip install&lt;/code&gt; to execute again from scratch.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No &lt;code&gt;.dockerignore&lt;/code&gt;:&lt;/strong&gt; Test files, git history, and local virtual environments were beamed right into the Docker daemon context.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Insecure Execution:&lt;/strong&gt; The app runs as &lt;code&gt;root&lt;/code&gt; (UID 0).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Development Server:&lt;/strong&gt; Flask’s built-in server is not built for production workloads.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Let's fix this step-by-step.&lt;/p&gt;




&lt;h2&gt;
  
  
  🛠️ The 6-Step Optimization Playbook
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Step 1: Switch to a Minimal Base Image (&lt;code&gt;-slim&lt;/code&gt;)
&lt;/h3&gt;

&lt;p&gt;The single highest-leverage change you can make is picking the right base image.&lt;/p&gt;

&lt;p&gt;Instead of the full &lt;code&gt;python:3.12&lt;/code&gt;, we switched to &lt;code&gt;python:3.12-slim&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; python:3.12-slim&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Why not Alpine (&lt;code&gt;python:3.12-alpine&lt;/code&gt;)?&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Alpine uses &lt;code&gt;musl&lt;/code&gt; libc instead of &lt;code&gt;glibc&lt;/code&gt;. While Alpine is tiny, Python packages with C extensions (like numpy, cryptography, etc.) frequently lack pre-compiled wheels for musl, triggering slow compilation during build or subtle runtime bugs. Debian slim is the sweet spot for Python: rock-solid compatibility with standard &lt;code&gt;glibc&lt;/code&gt; wheels and a tiny footprint.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Step 2: Ruthlessly Prune Unnecessary OS Packages
&lt;/h3&gt;

&lt;p&gt;Running &lt;code&gt;docker history myapp:baseline&lt;/code&gt; exposed where the bloat lived:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;apt-get update&lt;/code&gt; layer: &lt;strong&gt;~21.3 MB&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;curl git vim&lt;/code&gt; layer: &lt;strong&gt;~53.1 MB&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Neither &lt;code&gt;git&lt;/code&gt; nor &lt;code&gt;vim&lt;/code&gt; belong in a running container. If you need to debug a running container, use ephemeral debugging sidecars or mount volumes—don't permanently ship development utilities to production.&lt;/p&gt;

&lt;p&gt;We dropped the &lt;code&gt;apt-get&lt;/code&gt; commands entirely.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3: Add a Scrupulous &lt;code&gt;.dockerignore&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Whenever you run &lt;code&gt;docker build&lt;/code&gt;, Docker first transfers the entire directory (the "build context") to the Docker daemon.&lt;/p&gt;

&lt;p&gt;Without &lt;code&gt;.dockerignore&lt;/code&gt;, you're sending &lt;code&gt;.git&lt;/code&gt; logs, &lt;code&gt;.venv&lt;/code&gt;, &lt;code&gt;.pytest_cache&lt;/code&gt;, and temporary files.&lt;/p&gt;

&lt;p&gt;We added &lt;code&gt;.dockerignore&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;.git
.gitignore
__pycache__
.pytest_cache
.venv
tests
*.pyc
README.md
Dockerfile*
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This trimmed build context overhead and ensured sensitive or extraneous files could never leak into the container.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 4: Master Layer Caching (Order Matters!)
&lt;/h3&gt;

&lt;p&gt;Docker caches image layers. A layer is invalidated as soon as the files it depends on change.&lt;/p&gt;

&lt;p&gt;In our baseline:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="c"&gt;# ❌ BAD: Edits to app.py invalidate pip install cache&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; . .&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--no-cache-dir&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; requirements.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In our optimized build:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="c"&gt;# ✅ GOOD: Dependencies change rarely, application code changes frequently&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; requirements.txt .&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--no-cache-dir&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; requirements.txt

&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --chown=appuser:appuser app.py .&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, during local development, editing &lt;code&gt;app.py&lt;/code&gt; results in a rebuild that finishes in &lt;strong&gt;under a second&lt;/strong&gt; because the heavy &lt;code&gt;pip install&lt;/code&gt; layer is pulled straight from cache!&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 5: Adopt a Production WSGI Server (Gunicorn)
&lt;/h3&gt;

&lt;p&gt;Flask's built-in server warns you right in the logs:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"WARNING: This is a development server. Do not use it in a production deployment."&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;We added &lt;code&gt;gunicorn&lt;/code&gt; to &lt;code&gt;requirements.txt&lt;/code&gt; and updated our entrypoint:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;CMD&lt;/span&gt;&lt;span class="s"&gt; ["gunicorn", "--bind", "0.0.0.0:5000", "app:app"]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Gunicorn gives us process management, worker concurrency, and resilient request handling without bloating image size.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 6: Hardening Security — Run as Non-Root
&lt;/h3&gt;

&lt;p&gt;By default, Docker containers run as &lt;code&gt;root&lt;/code&gt;. If an attacker discovers a Remote Code Execution (RCE) vulnerability inside your app, they are root inside the container, making container breakout attacks significantly easier.&lt;/p&gt;

&lt;p&gt;We created an unprivileged system user and switched to it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;RUN &lt;/span&gt;useradd &lt;span class="nt"&gt;--create-home&lt;/span&gt; &lt;span class="nt"&gt;--shell&lt;/span&gt; /bin/bash appuser

&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --chown=appuser:appuser app.py .&lt;/span&gt;

&lt;span class="k"&gt;USER&lt;/span&gt;&lt;span class="s"&gt; appuser&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We can verify this directly on the running container:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker &lt;span class="nb"&gt;exec &lt;/span&gt;docker-opt-optimized &lt;span class="nb"&gt;whoami&lt;/span&gt;
&lt;span class="c"&gt;# Output: appuser&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  🏆 The "After": Hardened &amp;amp; Optimized Dockerfile
&lt;/h2&gt;

&lt;p&gt;Here is our final, production-ready &lt;code&gt;Dockerfile.optimized&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; python:3.12-slim&lt;/span&gt;

&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /app&lt;/span&gt;

&lt;span class="c"&gt;# 1. Leverage layer caching for dependencies&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; requirements.txt .&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--no-cache-dir&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; requirements.txt

&lt;span class="c"&gt;# 2. Security: Create dedicated unprivileged user&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;useradd &lt;span class="nt"&gt;--create-home&lt;/span&gt; &lt;span class="nt"&gt;--shell&lt;/span&gt; /bin/bash appuser

&lt;span class="c"&gt;# 3. Copy application code with proper ownership&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --chown=appuser:appuser app.py .&lt;/span&gt;

&lt;span class="c"&gt;# 4. Drop root privileges&lt;/span&gt;
&lt;span class="k"&gt;USER&lt;/span&gt;&lt;span class="s"&gt; appuser &lt;/span&gt;

&lt;span class="k"&gt;EXPOSE&lt;/span&gt;&lt;span class="s"&gt; 5000&lt;/span&gt;

&lt;span class="c"&gt;# 5. Production WSGI server&lt;/span&gt;
&lt;span class="k"&gt;CMD&lt;/span&gt;&lt;span class="s"&gt; ["gunicorn", "--bind", "0.0.0.0:5000", "app:app"]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Total lines: 18. Clean, readable, and lightning-fast.&lt;/p&gt;




&lt;h2&gt;
  
  
  📊 The Scorecard: Baseline vs. Optimized
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Baseline&lt;/th&gt;
&lt;th&gt;Optimized&lt;/th&gt;
&lt;th&gt;Difference&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Base Image&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;python:3.12&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;python:3.12-slim&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Streamlined&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Content Size&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;442 MB&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;56.6 MB&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;-385.4 MB (-87.2%)&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Uncompressed Disk&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1.75 GB&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;256 MB&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;-1.49 GB (-85.4%)&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Extra OS Tools&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;curl&lt;/code&gt;, &lt;code&gt;git&lt;/code&gt;, &lt;code&gt;vim&lt;/code&gt; (~74 MB)&lt;/td&gt;
&lt;td&gt;Zero&lt;/td&gt;
&lt;td&gt;Clean runtime&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Layer Caching&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Broken on every commit&lt;/td&gt;
&lt;td&gt;Optimized&lt;/td&gt;
&lt;td&gt;Instant rebuilds&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;User&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;root&lt;/code&gt; (UID 0)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;appuser&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Least privilege&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Web Server&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Dev server&lt;/td&gt;
&lt;td&gt;Gunicorn WSGI&lt;/td&gt;
&lt;td&gt;Production ready&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  🥊 Real-World Gotchas &amp;amp; Lessons Learned
&lt;/h2&gt;

&lt;p&gt;Optimization isn't just about shaving megabytes in a spreadsheet—here are real obstacles encountered during the project:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. The "Host Port Already in Use" Trap
&lt;/h3&gt;

&lt;p&gt;When launching the container:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker run &lt;span class="nt"&gt;-p&lt;/span&gt; 5000:5000 myapp:optimized
&lt;span class="c"&gt;# Error: bind: address already in use&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On macOS, port &lt;code&gt;5000&lt;/code&gt; is frequently taken by the OS AirPlay Receiver service.&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Solution:&lt;/strong&gt; Understand Docker's port mapping format (&lt;code&gt;HOST_PORT:CONTAINER_PORT&lt;/code&gt;).&lt;br&gt;&lt;br&gt;
We mapped &lt;code&gt;-p 5001:5000&lt;/code&gt;, letting the internal app stay on &lt;code&gt;5000&lt;/code&gt; while exposing it cleanly on host port &lt;code&gt;5001&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker run &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;--name&lt;/span&gt; docker-opt-optimized &lt;span class="nt"&gt;-p&lt;/span&gt; 5001:5000 docker-image-optimization:optimized
curl http://localhost:5001/health
&lt;span class="c"&gt;# {"status":"healthy"}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. "Disk Usage" vs "Content Size"
&lt;/h3&gt;

&lt;p&gt;When running &lt;code&gt;docker images&lt;/code&gt;, Docker may report one size, while &lt;code&gt;docker system df -v&lt;/code&gt; or registry push reports another.  &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Content Size:&lt;/strong&gt; The compressed size of layers transferred across networks/registries (56.6 MB).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Disk Usage:&lt;/strong&gt; The uncompressed layer footprint unpacked on the host filesystem (256 MB vs 1.75 GB).
Always use consistent metrics when publishing benchmarks!&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  3. Never Optimize Without Validation
&lt;/h3&gt;

&lt;p&gt;An image with 0 MB size that crashes is useless. After trimming the image, always test both endpoints and run automated test suites:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Automated tests via pytest&lt;/span&gt;
mise &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="nt"&gt;--&lt;/span&gt; pytest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;tests/test_app.py ..                             [100%]
====================== 2 passed in 0.08s =======================
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  💡 Quick Docker Optimization Checklist for Your Projects
&lt;/h2&gt;

&lt;p&gt;Save this checklist for your next Dockerfile:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Use &lt;code&gt;-slim&lt;/code&gt; or minimal official base images.&lt;/li&gt;
&lt;li&gt;[ ] Maintain a &lt;code&gt;.dockerignore&lt;/code&gt; containing &lt;code&gt;.git&lt;/code&gt;, caches, virtual environments, and tests.&lt;/li&gt;
&lt;li&gt;[ ] Copy &lt;code&gt;requirements.txt&lt;/code&gt; / &lt;code&gt;package.json&lt;/code&gt; &lt;strong&gt;before&lt;/strong&gt; copying application code.&lt;/li&gt;
&lt;li&gt;[ ] Remove &lt;code&gt;curl&lt;/code&gt;, &lt;code&gt;vim&lt;/code&gt;, &lt;code&gt;git&lt;/code&gt;, and build tools from final production images.&lt;/li&gt;
&lt;li&gt;[ ] Use &lt;code&gt;--no-cache-dir&lt;/code&gt; (Python) or &lt;code&gt;--no-cache&lt;/code&gt; / clean commands when installing dependencies.&lt;/li&gt;
&lt;li&gt;[ ] Create and switch to a non-root &lt;code&gt;USER&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;[ ] Replace development servers with production application servers (Gunicorn, Uvicorn, Nginx).&lt;/li&gt;
&lt;li&gt;[ ] Validate image behavior with health checks and unit tests.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  💬 Over to You!
&lt;/h2&gt;

&lt;p&gt;Have you ever inspected your production Docker images with &lt;code&gt;docker history&lt;/code&gt; and found unexpected surprises? What's your favorite trick for shrinking containers? Drop your thoughts in the comments below! 👇&lt;/p&gt;

</description>
      <category>devops</category>
      <category>docker</category>
      <category>linux</category>
      <category>containers</category>
    </item>
    <item>
      <title>I tried to live an entire day inside apps built with Expo. Here's where it broke.</title>
      <dc:creator>Dan</dc:creator>
      <pubDate>Thu, 10 Sep 2026 22:07:28 +0000</pubDate>
      <link>https://dev.to/expo/i-tried-to-live-an-entire-day-inside-apps-built-with-expo-heres-where-it-broke-j5d</link>
      <guid>https://dev.to/expo/i-tried-to-live-an-entire-day-inside-apps-built-with-expo-heres-where-it-broke-j5d</guid>
      <description>&lt;p&gt;There are thousands of apps on the App Store and Google Play running on &lt;a href="https://expo.dev" rel="noopener noreferrer"&gt;Expo&lt;/a&gt;'s open source tooling. That's a big number, but it doesn't tell you much. What I actually wanted to know: do those apps cover a full day, start to finish, without me reaching for anything else on my phone?&lt;/p&gt;

&lt;p&gt;So I ran the experiment. One day, Expo apps only. By breakfast I'd paid a celebrity to record a video. By lunch I was generating an app from my phone. By late afternoon I was sweating through a shirt. By bedtime I was too tired to feel smug about any of it.&lt;/p&gt;

&lt;p&gt;Short answer: yes, it works. The longer answer is more useful, because the moments where the day fell apart are exactly the moments where somebody should go build something.&lt;/p&gt;

&lt;h2&gt;
  
  
  Morning
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://apps.apple.com/us/app/rise-sleep-tracker/id1453884781" rel="noopener noreferrer"&gt;Rise&lt;/a&gt;, sleep tracking and energy forecasts&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://apps.apple.com/us/app/espresso-world-news-in-a-shot/id896628003" rel="noopener noreferrer"&gt;Espresso&lt;/a&gt;, news from The Economist&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://apps.apple.com/us/app/weatherwise-app/id6736407724" rel="noopener noreferrer"&gt;WeatherWise&lt;/a&gt;, forecast and live radar&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://apps.apple.com/us/app/epa-airnow/id467653238" rel="noopener noreferrer"&gt;AirNow&lt;/a&gt;, air quality index from the US EPA&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://apps.apple.com/us/app/delish-original-fun-recipes/id6747781542" rel="noopener noreferrer"&gt;Delish&lt;/a&gt;, recipes with video steps&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;First app of the day was Rise, a sleep tracker that also predicts when you'll get energy spikes and when your body wants to wind down. Genuinely useful for planning a day this ridiculous.&lt;/p&gt;

&lt;p&gt;Next, Espresso, from The Economist. News in small, fast hits, which is the whole premise. Content-heavy apps like this have always been a strong spot for Expo, and it shows: an app like this lives on navigation and linking, which is exactly the job &lt;a href="https://expo.dev/router" rel="noopener noreferrer"&gt;Expo Router&lt;/a&gt; is built for.&lt;/p&gt;

&lt;p&gt;I needed to know if the weather and air quality would cooperate with my afternoon plans. WeatherWise covered the forecast and live radar, feeling like the pro version of the weather app that already ships on your phone. For air quality I used AirNow, from the US EPA. It gives you the current index and the outlook for the week. The UI could use some attention, and I'll say that plainly, but it works, and there's something quietly great about a federal agency reaching for Expo to help people figure out if it's safe to go outside during wildfire season.&lt;/p&gt;

&lt;p&gt;I'm the cook in my house, so I opened Delish for dinner ideas. Hand-picked recipes with real videos and step-by-step instructions, no eight-paragraph story about someone's grandmother sitting on top of the ingredient list. Do not browse it hungry.&lt;/p&gt;

&lt;h2&gt;
  
  
  The messaging gap
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://apps.apple.com/us/app/cameo-personal-celeb-videos/id1258311581" rel="noopener noreferrer"&gt;Cameo&lt;/a&gt;, personalized videos from celebrities&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Normally around here I'd text my wife what I'm making for dinner. This is where the day fell apart, because there isn't really a messaging app built with Expo. We use iMessage, and iMessage was off the table.&lt;/p&gt;

&lt;p&gt;The problem with messaging apps is that shipping one isn't enough. Both sides of the conversation need it. I can install an app for myself, but I can't install one on my wife's expectations about how she wants to be reached. That's a real gap in what's out there.&lt;/p&gt;

&lt;p&gt;So I got someone else to text her instead. Cameo runs on Expo, and their team has a &lt;a href="https://expo.dev/customers/cameo" rel="noopener noreferrer"&gt;case study&lt;/a&gt; about scaling through a stretch of hyper-growth on it. Cameo is the app where you pay a celebrity to record a personal video for someone. My wife and I watch a lot of Food Network, so I tracked down a Food Network star who could deliver same-day. Was this the intended use case? No. Was it cheap? Also no.&lt;/p&gt;

&lt;h2&gt;
  
  
  Afternoon
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://apps.apple.com/us/app/tesla/id582007913" rel="noopener noreferrer"&gt;Tesla&lt;/a&gt;, navigation and car controls&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://apps.apple.com/us/app/tommys-express/id1084713029" rel="noopener noreferrer"&gt;Tommy's Express&lt;/a&gt;, drive-through car wash&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://apps.apple.com/us/app/replit-vibe-code-apps/id1614022293" rel="noopener noreferrer"&gt;Replit&lt;/a&gt;, coding with a built-in agent&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://apps.apple.com/us/app/mercari-buying-selling-app/id896130944" rel="noopener noreferrer"&gt;Mercari&lt;/a&gt;, second-hand marketplace&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I found a car wash running on Expo, then realized I had no way to get there since there's no Expo maps app either. The Tesla app has navigation built in and also controls the car itself, which solved two problems with one tap.&lt;/p&gt;

&lt;p&gt;Once I got there, the Tommy's Express app let me use the car wash without talking to another human. Pull into the lane the app tells you, it reads your plate, it already has your plan and card on file, gate opens. If you're an introvert, this is close to a religious experience.&lt;/p&gt;

&lt;p&gt;Then I set up at a cafe to get some actual work done, using the Replit app. A few people in our YouTube comments have asked for a mobile app to check EAS builds and workflows on the go, and this felt like the right day to start one. Replit has a coding agent built in, so I had it generate a first draft.&lt;/p&gt;

&lt;p&gt;I also spent some time in Mercari, a second-hand marketplace where people sell the good stuff out of their closets. My feed that day was, for reasons I can't explain, an oops-all-hats situation. Found one that's extremely Silicon Valley coded, so I might go back for it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Evening
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://apps.apple.com/us/app/runna-running-plans-coach/id1594204443" rel="noopener noreferrer"&gt;Runna&lt;/a&gt;, personalized running plans&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://apps.apple.com/us/app/rover-pet-sitters/id547320928" rel="noopener noreferrer"&gt;Rover&lt;/a&gt;, pet sitting and dog walking&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://apps.apple.com/us/app/once-upon-photo-book-creator/id1187208815" rel="noopener noreferrer"&gt;Once Upon&lt;/a&gt;, printed photo books&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://apps.apple.com/us/app/voidpet-garden-mental-health/id1668932264" rel="noopener noreferrer"&gt;Voidpet Garden&lt;/a&gt;, self-reflection as a game&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://apps.apple.com/us/app/book-of-the-month/id1473873226" rel="noopener noreferrer"&gt;Book of the Month&lt;/a&gt;, a short list of books to read&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I have a race coming up this fall, so I used Runna to track a training run. Tell it a bit about yourself and it writes a personalized plan, whether you're training for your first 5K or something longer. This is where the sweating happened.&lt;/p&gt;

&lt;p&gt;After dinner I walked the dog, which reminded me we need a sitter for an upcoming trip. Rover is a genuinely nice app for finding people to look after your pet. When I lived in New York City, finding boarding with actual availability was hard. The sitters I found through Rover were kind enough to my dog that I'd book them again without thinking twice.&lt;/p&gt;

&lt;p&gt;I also used Once Upon to turn photos out of my library into a printed book. It has short videos showing what hardcover versus softcover and matte versus glossy actually look like, a detail most apps skip entirely, then let me drag photos around until the layout felt right.&lt;/p&gt;

&lt;p&gt;To unwind, I played Voidpet Garden, made by Ben Awad. It turns self-reflection into a game: you catch creatures, tend a garden, fight the occasional battle, and along the way it asks how your day went and how things made you feel. We could use more of that.&lt;/p&gt;

&lt;p&gt;Last thing before bed was the Book of the Month Club app. Walking into a bookstore means facing a few thousand choices at once. This trims it to five or seven. I picked one called The Last Contract of Esequibo, mostly for the tagline: "Live by the code or die by the knife." Felt about right for development life these days.&lt;/p&gt;

&lt;h2&gt;
  
  
  Loose ends
&lt;/h2&gt;

&lt;p&gt;Three things resolved after the day was already over.&lt;/p&gt;

&lt;p&gt;The Cameo video didn't arrive that evening. Celebrities, it turns out, have schedules. It showed up a few days later, we loved it, and now I owe my wife that dinner all over again.&lt;/p&gt;

&lt;p&gt;The photo book arrived, and it's genuinely nice.&lt;/p&gt;

&lt;p&gt;And the EAS companion app I started in Replit made real progress. I showed it around internally, more people got excited than I expected, and we're now actively working on it. Expect something to look at later this year.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the gaps tell you
&lt;/h2&gt;

&lt;p&gt;The three places the day broke were messaging, maps, and camera, and they're really the same gap wearing different clothes.&lt;/p&gt;

&lt;p&gt;Messaging is hard because it needs two people to adopt it at once. Maps and camera are hard for the opposite reason: your phone already ships with a good-enough version, and good-enough is genuinely difficult to beat. Which is exactly why these categories are worth building into.&lt;/p&gt;

&lt;p&gt;One of the goals at Expo is letting more people build for mobile. The screen in your pocket is the one most of us stare at all day, for better or worse, and everyone should be able to build for it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Start something
&lt;/h2&gt;

&lt;p&gt;If one of those gaps sounds like your idea, here's the whole path in: &lt;a href="https://docs.expo.dev/get-started/introduction/" rel="noopener noreferrer"&gt;the getting started guide&lt;/a&gt; walks you to a build on a real device, and &lt;a href="https://expo.dev/eas" rel="noopener noreferrer"&gt;EAS Build&lt;/a&gt; takes it to the app stores without you ever opening Xcode or Android Studio. If you get stuck along the way, come find us in &lt;a href="https://chat.expo.dev" rel="noopener noreferrer"&gt;Discord&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;This post is based on content from the &lt;a href="https://expo.dev/blog/24-hours-of-expo" rel="noopener noreferrer"&gt;Expo blog&lt;/a&gt;. Follow &lt;a href="https://dev.to/expo"&gt;@expo&lt;/a&gt; for more React Native content.&lt;/p&gt;

</description>
      <category>expo</category>
      <category>mobile</category>
      <category>javascript</category>
    </item>
    <item>
      <title>Metrice: Zero-dependency post-quantum P2P mesh network</title>
      <dc:creator>Ahmet Göktürk</dc:creator>
      <pubDate>Thu, 10 Sep 2026 22:06:31 +0000</pubDate>
      <link>https://dev.to/gokturka/metrice-zero-dependency-post-quantum-p2p-mesh-network-33ae</link>
      <guid>https://dev.to/gokturka/metrice-zero-dependency-post-quantum-p2p-mesh-network-33ae</guid>
      <description>&lt;p&gt;Metrice is a decentralized peer-to-peer (P2P) mesh networking protocol engineered with zero external npm dependencies (Zero-Dependency), running natively on Node.js core libraries (&lt;code&gt;node:crypto&lt;/code&gt;, &lt;code&gt;node:net&lt;/code&gt;, &lt;code&gt;node:dgram&lt;/code&gt;, &lt;code&gt;node:sqlite&lt;/code&gt;, &lt;code&gt;node:dns&lt;/code&gt;). It features quantum-resistant cryptography (Post-Quantum Cryptography) and a Tor-like multi-hop onion routing architecture.&lt;/p&gt;

&lt;p&gt;The system incorporates NIST FIPS 203 ML-KEM-768 key encapsulation, Ed25519-based RFC 4648 Base32 cryptographic node identities, AutoNAT dialback consensus, Rendezvous persistent reverse tunnels for CGNAT traversal, multi-relay transit bridging (EDGE Transit Routing / &lt;code&gt;CAP_EDGE_TRANSIT&lt;/code&gt;), Layer 4 HAProxy PROXY Protocol v1 &amp;amp; v2 support, and an embedded in-memory SSH-2 server.&lt;/p&gt;




&lt;h2&gt;
  
  
  Architecture &amp;amp; Core Components
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Cryptographic Node Identity &amp;amp; Addressing
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Every node maintains a persistent Ed25519 identity key pair.&lt;/li&gt;
&lt;li&gt;The 16-character Node ID (&lt;code&gt;NodeID&lt;/code&gt;) is derived from the first 10 bytes (80 bits) of the SHA-256 digest of the raw Ed25519 public key encoded in RFC 4648 Base32 (&lt;code&gt;^[a-z2-7]{16}$&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Network addressing is completely IP/Port agnostic using virtual &lt;code&gt;.mesh&lt;/code&gt; domain namespaces:

&lt;ul&gt;
&lt;li&gt;User Address: &lt;code&gt;@user:NodeID.mesh&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Federated Channel: &lt;code&gt;#channel:NodeID.mesh&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Global Mesh Channel: &lt;code&gt;#genel&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  2. AutoNAT &amp;amp; Reachability Consensus
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Nodes exchange their observed peer addresses (&lt;code&gt;observedAddress&lt;/code&gt;) during the cryptographic handshake.&lt;/li&gt;
&lt;li&gt;A Reflected Public IP consensus is established once at least two independent peers report consistent observations.&lt;/li&gt;
&lt;li&gt;Nodes initiate reachability testing by transmitting a &lt;code&gt;DIALBACK_REQUEST&lt;/code&gt; containing a cryptographic nonce.&lt;/li&gt;
&lt;li&gt;The target peer attempts a TCP dialback connection to the requesting node's physical remote address (&lt;code&gt;socket.realRemoteAddress || socket.remoteAddress&lt;/code&gt;). If verified, the node attains the &lt;code&gt;CAP_RELAY&lt;/code&gt; role; otherwise, it remains in &lt;code&gt;CAP_EDGE&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SSRF Defense:&lt;/strong&gt; Injected &lt;code&gt;targetIp&lt;/code&gt; values inside &lt;code&gt;DIALBACK_REQUEST&lt;/code&gt; are strictly discarded; only the verified physical TCP socket remote address is used. Dialbacks targeting RFC 1918 private networks or loopback addresses are blocked.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  3. Rendezvous, CGNAT Reverse Tunnels &amp;amp; Transit Routing (CAP_EDGE_TRANSIT)
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Firewalled or CGNAT-bound &lt;code&gt;EDGE&lt;/code&gt; nodes establish persistent reverse TCP tunnels to multiple publicly reachable &lt;code&gt;RELAY&lt;/code&gt; nodes (&lt;code&gt;maxEdgeRendezvousRelays&lt;/code&gt;, default: 4).&lt;/li&gt;
&lt;li&gt;Tunnel sessions are authenticated via Ed25519 cryptographic signatures in &lt;code&gt;RENDEZVOUS_BIND&lt;/code&gt; packets.&lt;/li&gt;
&lt;li&gt;Firewall session state is preserved through 30-second single-byte keepalives: &lt;code&gt;0x09&lt;/code&gt; (PING) and &lt;code&gt;0x0A&lt;/code&gt; (PONG).&lt;/li&gt;
&lt;li&gt;Active tunnel capacity per relay is bounded to 64 to prevent resource exhaustion (&lt;code&gt;maxRendezvousTunnels&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dynamic Role Escalation (&lt;code&gt;CAP_EDGE_TRANSIT&lt;/code&gt;):&lt;/strong&gt; An EDGE node connected to at least two independent relays with &lt;code&gt;ALLOW_EDGE_ROUTING=true&lt;/code&gt; dynamically ascends to &lt;code&gt;CAP_EDGE_TRANSIT&lt;/code&gt;, enabling bidirectional in-and-out reverse tunnel bridging between segmented relays.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Loop-Free Gossip Bridging:&lt;/strong&gt; Transit edge nodes cross-bridge presence announcements (&lt;code&gt;PRESENCE_ANNOUNCE&lt;/code&gt;) and global &lt;code&gt;#genel&lt;/code&gt; messages between relays without broadcast loops (&lt;code&gt;ALLOW_EDGE_GOSSIP=true&lt;/code&gt;).&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  4. 3-Hop Telescopic Post-Quantum Onion Routing
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Anonymous 3-hop circuits (Inbound Guard, Relay/Transit, Outbound Exit) conceal network topology and packet trajectories.&lt;/li&gt;
&lt;li&gt;The circuit selection pool (&lt;code&gt;relayPool&lt;/code&gt;) integrates both backbone &lt;code&gt;RELAY&lt;/code&gt; nodes and &lt;code&gt;CAP_EDGE_TRANSIT&lt;/code&gt; nodes to enhance routing diversity.&lt;/li&gt;
&lt;li&gt;Each hop negotiates ephemeral symmetric keys via NIST FIPS 203 ML-KEM-768 (Kyber-768) key encapsulation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Traffic Analysis &amp;amp; DPI Resistance:&lt;/strong&gt; All onion cells (&lt;code&gt;ONION_CELL&lt;/code&gt;) are padded to a strict uniform length of 2048 bytes (Uniform Cell Padding). Raw payloads are capped at 768 bytes (&lt;code&gt;MAX_ONION_PAYLOAD&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Cells are never exposed in plaintext; transport is secured inside AES-256-GCM &lt;code&gt;ENCRYPTED_FRAME&lt;/code&gt; blocks.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  5. Distributed Presence &amp;amp; SQLite Routing
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Presence and channel subscriptions are propagated across the mesh using Ed25519-signed &lt;code&gt;PRESENCE_ANNOUNCE&lt;/code&gt; gossip packets.&lt;/li&gt;
&lt;li&gt;Raw IP addresses are scrubbed from gossip frames; announcements reference only virtual domain names or &lt;code&gt;.mesh&lt;/code&gt; identifiers.&lt;/li&gt;
&lt;li&gt;Ephemeral routing entries are cached in memory and committed to the SQLite &lt;code&gt;routing_table&lt;/code&gt;. Inactive records expire automatically after 60 seconds (TTL).&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  6. In-Memory SSH-2 Server &amp;amp; Two-Factor Vault Authentication
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Pure JavaScript SSH-2 server operates natively without requiring external system daemons (&lt;code&gt;sshd&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dynamic Version Synchronisation:&lt;/strong&gt; Server identification string (&lt;code&gt;sshServerVersion&lt;/code&gt;) dynamically aligns with &lt;code&gt;package.json&lt;/code&gt; through &lt;code&gt;src/version.js&lt;/code&gt; (default: &lt;code&gt;SSH-2.0-Metrice_2.6.0&lt;/code&gt;) and remains configurable via environment variables.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Two-Factor Ephemeral Vault Derivation:&lt;/strong&gt; User passwords are salted with the client's Ed25519 public key and derived via Scrypt (N=16384, r=8, p=1) and HKDF-SHA256. Authentication fails without the registered physical Ed25519 key, even if the password is correct.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  7. Layer 4 HAProxy PROXY Protocol v1 &amp;amp; v2 Support
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Nodes operating behind Layer 4 reverse proxies (HAProxy, Nginx Stream, AWS NLB) transparently resolve real client IP addresses and ports (&lt;code&gt;realRemoteAddress&lt;/code&gt;, &lt;code&gt;realRemotePort&lt;/code&gt;) with &lt;code&gt;USE_PROXY_PROTOCOL=true&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Supports both US-ASCII text PROXY v1 (&lt;code&gt;PROXY TCP4/TCP6/UNKNOWN&lt;/code&gt;) and 12-byte binary magic PROXY v2 with zero external libraries.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;IP Spoofing Immunity:&lt;/strong&gt; Only proxies specified in &lt;code&gt;PROXY_TRUSTED_IPS&lt;/code&gt; (default: &lt;code&gt;127.0.0.1,::1&lt;/code&gt;) are authorized. Unauthorized spoofing attempts are instantly rejected with immediate socket termination (&lt;code&gt;status: REJECT&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Transparent Passthrough:&lt;/strong&gt; Direct connections without PROXY headers have unparsed bytes restored (&lt;code&gt;socket.unshift(remainder)&lt;/code&gt;) and route seamlessly to federation, SSH, or Telnet handlers with zero data loss.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To see the full installation guide, deployment models, and deployment via Docker, check out the official repository:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Full Documentation &amp;amp; Source Code:&lt;/strong&gt; &lt;a href="https://github.com/GokturkA1/metrice" rel="noopener noreferrer"&gt;https://github.com/GokturkA1/metrice&lt;/a&gt;&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>node</category>
      <category>security</category>
      <category>distributedsystems</category>
    </item>
    <item>
      <title>Automating Pull Request Workflows with Claude Task Master</title>
      <dc:creator>developerz.ai</dc:creator>
      <pubDate>Thu, 10 Sep 2026 22:05:30 +0000</pubDate>
      <link>https://dev.to/developerzai/automating-pull-request-workflows-with-claude-task-master-2me2</link>
      <guid>https://dev.to/developerzai/automating-pull-request-workflows-with-claude-task-master-2me2</guid>
      <description>&lt;h1&gt;
  
  
  Introduction
&lt;/h1&gt;

&lt;p&gt;Developers spend a lot of time managing pull requests, fixing CI failures, and responding to review comments. Claude Task Master provides a command line interface that automates this entire loop. By giving the tool a high level goal, it plans the work, writes code, pushes commits, opens a pull request, handles CI, addresses feedback, and merges when all checks pass.&lt;/p&gt;

&lt;h1&gt;
  
  
  How It Works
&lt;/h1&gt;

&lt;p&gt;The CLI reads the repository, creates a task list, and defines success criteria before any code is changed. Each task is executed in isolation: the tool makes the required modifications, runs the test suite, and creates a commit. After the commit is pushed, a pull request is opened automatically. The pull request enters the CI stage; if any checks fail, Claude Task Master updates the code, runs the tests again, and pushes a new commit. Review comments are fetched via the GitHub API, and the tool can apply suggested changes without human intervention. When the pull request passes all checks and receives approval, the tool performs an auto merge.&lt;/p&gt;

&lt;h1&gt;
  
  
  Benefits
&lt;/h1&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Hands off&lt;/strong&gt; - set a goal and let the tool run until the pull request is merged.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;State persistence&lt;/strong&gt; - the CLI stores its progress on disk, so a stopped session can resume exactly where it left off.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Profile isolation&lt;/strong&gt; - multiple Claude subscriptions can be used in parallel by creating separate profiles, each with its own configuration directory.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Extensible integration&lt;/strong&gt; - a REST API, an MCP server, and signed webhooks expose the same lifecycle to external systems, enabling custom dashboards or CI pipelines.&lt;/li&gt;
&lt;/ul&gt;

&lt;h1&gt;
  
  
  Example Usage
&lt;/h1&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Install the CLI (uv, pip, or Docker are supported)&lt;/span&gt;
uv tool &lt;span class="nb"&gt;install &lt;/span&gt;claude-task-master

&lt;span class="c"&gt;# Authenticate with Claude Code&lt;/span&gt;
claude login

&lt;span class="c"&gt;# Run a task in your project directory&lt;/span&gt;
&lt;span class="nb"&gt;cd &lt;/span&gt;my-project
claudetm start &lt;span class="s2"&gt;"Add user authentication with tests"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The command above creates a plan, writes the authentication code, adds tests, pushes the changes, opens a pull request, and watches the CI pipeline. If the CI fails, the tool fixes the issue and pushes a new commit. Once the pull request is approved, it merges automatically.&lt;/p&gt;

&lt;h1&gt;
  
  
  Integration Points
&lt;/h1&gt;

&lt;p&gt;Claude Task Master can be invoked from other tools via its REST API. For example, a CI job can POST a JSON payload with a goal description, and the server will start a new task. Webhooks can be configured to notify a Slack channel when a pull request is merged or when a CI failure occurs. The MCP server provides a lightweight message queue for coordinating multiple instances of the CLI.&lt;/p&gt;

&lt;h1&gt;
  
  
  Conclusion
&lt;/h1&gt;

&lt;p&gt;Automating the pull request workflow reduces manual overhead and speeds up delivery. Claude Task Master combines planning, execution, and verification in a single tool that works with the Claude Code session or a direct Anthropic compatible API. It is open source under the MIT license and can be installed via PyPI or Docker. Try it today and see how a fully autonomous pull request loop can improve your development process.&lt;/p&gt;




&lt;p&gt;Repository: &lt;a href="https://github.com/developerz-ai/claude-task-master" rel="noopener noreferrer"&gt;https://github.com/developerz-ai/claude-task-master&lt;/a&gt;&lt;/p&gt;

</description>
      <category>automation</category>
      <category>claude</category>
      <category>cli</category>
      <category>softwaredevelopment</category>
    </item>
  </channel>
</rss>
