<?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: ctrl</title>
    <description>The latest articles on DEV Community by ctrl (@mrvlyouknowwho).</description>
    <link>https://dev.to/mrvlyouknowwho</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4013770%2F50bce262-0f15-4ad3-9717-231da0d21b6e.jpg</url>
      <title>DEV Community: ctrl</title>
      <link>https://dev.to/mrvlyouknowwho</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/mrvlyouknowwho"/>
    <language>en</language>
    <item>
      <title>Push-and-pray CI is dumb. I built a step debugger for GitHub Actions.</title>
      <dc:creator>ctrl</dc:creator>
      <pubDate>Fri, 03 Jul 2026 14:54:59 +0000</pubDate>
      <link>https://dev.to/mrvlyouknowwho/push-and-pray-ci-is-dumb-i-built-a-step-debugger-for-github-actions-15an</link>
      <guid>https://dev.to/mrvlyouknowwho/push-and-pray-ci-is-dumb-i-built-a-step-debugger-for-github-actions-15an</guid>
      <description>&lt;p&gt;You know the loop.&lt;/p&gt;

&lt;p&gt;You edit a &lt;code&gt;.yml&lt;/code&gt;. You commit. You push. You wait for the runner to spin up. You watch it churn through five green steps and then fail on step six — some path that isn't set, some env var that didn't thread through, a script that behaves differently than you remembered. You tweak one line. Commit. Push. Wait again.&lt;/p&gt;

&lt;p&gt;Every iteration of that loop costs you a commit you'll squash later and three-to-five minutes of staring at a log that scrolls on a server you can't touch. The feedback is slow &lt;em&gt;and&lt;/em&gt; it's blind: when step six fails, you can't get &lt;strong&gt;into&lt;/strong&gt; the machine at the moment it failed and look around. You just get the corpse in the log.&lt;/p&gt;

&lt;p&gt;I got tired of it and built &lt;strong&gt;&lt;a href="https://github.com/mrvlyouknowwho/walkflow" rel="noopener noreferrer"&gt;walkflow&lt;/a&gt;&lt;/strong&gt; — a tool that runs your workflow's steps &lt;strong&gt;on your own machine, one at a time&lt;/strong&gt;, and pauses between them so you can inspect, fix, and continue. No commit. No push. No waiting.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it feels like
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;walkflow
&lt;span class="go"&gt;walkflow — .github/workflows/ci.yml · job 'build'

▶ job build — 6 step(s)

┌─ step 1/6: checkout
│  uses: actions/checkout@v4 — not executable in host mode, skipping.

┌─ step 2/6: install deps
&lt;/span&gt;&lt;span class="gp"&gt;│    $&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;npm ci
&lt;span class="gp"&gt;│  [enter] run · [s]hell · [k] skip · [q] quit &amp;gt;&lt;/span&gt;&lt;span class="w"&gt;           &lt;/span&gt;&lt;span class="c"&gt;# ⏎&lt;/span&gt;
&lt;span class="go"&gt;│  running in /home/me/app
└─ ok

┌─ step 3/6: run tests
&lt;/span&gt;&lt;span class="gp"&gt;│    $&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;npm &lt;span class="nb"&gt;test&lt;/span&gt;
&lt;span class="gp"&gt;│  [enter] run · [s]hell · [k] skip · [q] quit &amp;gt;&lt;/span&gt;&lt;span class="w"&gt;           &lt;/span&gt;&lt;span class="c"&gt;# ⏎&lt;/span&gt;
&lt;span class="go"&gt;FAIL src/auth.test.ts  ✗ token refresh
&lt;/span&gt;&lt;span class="gp"&gt;│  step failed. [r]etry · [s]hell · [c]ontinue · [q]uit &amp;gt;&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="c"&gt;# s&lt;/span&gt;
&lt;span class="gp"&gt;│  entering shell (/bin/zsh);&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="sb"&gt;`&lt;/span&gt;&lt;span class="nb"&gt;exit&lt;/span&gt;&lt;span class="sb"&gt;`&lt;/span&gt; to &lt;span class="k"&gt;return &lt;/span&gt;to walkflow
&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="nv"&gt;$NODE_ENV&lt;/span&gt;        &lt;span class="c"&gt;# inspect the exact env step 3 saw&lt;/span&gt;
&lt;span class="go"&gt;test
&lt;/span&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;vim src/auth.ts       &lt;span class="c"&gt;# fix it right here&lt;/span&gt;
&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;exit&lt;/span&gt;
&lt;span class="go"&gt;│  back in walkflow
&lt;/span&gt;&lt;span class="gp"&gt;│  step failed. [r]etry · [s]hell · [c]ontinue · [q]uit &amp;gt;&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="c"&gt;# r&lt;/span&gt;
&lt;span class="go"&gt;└─ ok
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The important part is the &lt;code&gt;s&lt;/code&gt;. When a step fails — or before any step runs — you can drop into a shell &lt;strong&gt;in the workspace, with every environment variable the previous steps exported&lt;/strong&gt;. Not a fresh shell. The exact state that step would have seen: the &lt;code&gt;PATH&lt;/code&gt; additions from earlier steps, the values written to &lt;code&gt;$GITHUB_ENV&lt;/code&gt;, the working directory. You poke around, figure out what's wrong, fix the file right there, &lt;code&gt;exit&lt;/code&gt;, and hit &lt;code&gt;r&lt;/code&gt; to retry — and &lt;code&gt;r&lt;/code&gt; even offers to open the failing command in &lt;code&gt;$EDITOR&lt;/code&gt; first.&lt;/p&gt;

&lt;p&gt;The inner loop that used to be "push and pray, wait four minutes" is now seconds long.&lt;/p&gt;

&lt;h2&gt;
  
  
  "Isn't that what &lt;code&gt;act&lt;/code&gt; does?"
&lt;/h2&gt;

&lt;p&gt;Sort of, and this is the first question everyone asks, so let me be precise.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/nektos/act" rel="noopener noreferrer"&gt;&lt;code&gt;act&lt;/code&gt;&lt;/a&gt; runs your whole workflow locally in Docker. It's great for &lt;em&gt;replaying&lt;/em&gt; — top to bottom, start to finish, then it stops. What it doesn't do is &lt;strong&gt;pause between steps&lt;/strong&gt;, drop you into the live intermediate state, or let you edit a failed step and re-run it in place. That's been &lt;a href="https://github.com/nektos/act/issues/1050" rel="noopener noreferrer"&gt;an open feature request on &lt;code&gt;act&lt;/code&gt; for years&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;walkflow is built around exactly that missing piece — the interactive inner loop, not the full replay:&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;&lt;code&gt;act&lt;/code&gt;&lt;/th&gt;
&lt;th&gt;&lt;code&gt;walkflow&lt;/code&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Run workflow locally&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pause &lt;strong&gt;between&lt;/strong&gt; steps&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Drop into a shell with live step state&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Edit a failed step and retry in place&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Faithful &lt;code&gt;$GITHUB_ENV&lt;/code&gt; / &lt;code&gt;$GITHUB_PATH&lt;/code&gt; threading&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;They're not really competitors. &lt;code&gt;act&lt;/code&gt; answers "does the whole thing pass in a container?" walkflow answers "why is step six broken, and can I fix it without pushing eight times?"&lt;/p&gt;

&lt;h2&gt;
  
  
  What it faithfully reproduces
&lt;/h2&gt;

&lt;p&gt;I care about this being honest, because a debugger that lies to you is worse than no debugger. walkflow reproduces:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Environment layering: workflow &lt;code&gt;env:&lt;/code&gt; → job &lt;code&gt;env:&lt;/code&gt; → step &lt;code&gt;env:&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;$GITHUB_ENV&lt;/code&gt;&lt;/strong&gt; exports — both &lt;code&gt;KEY=value&lt;/code&gt; and the &lt;code&gt;KEY&amp;lt;&amp;lt;EOF&lt;/code&gt; heredoc form — threaded into later steps.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;$GITHUB_PATH&lt;/code&gt;&lt;/strong&gt; additions prepended to &lt;code&gt;PATH&lt;/code&gt; for later steps.&lt;/li&gt;
&lt;li&gt;The default step shell (&lt;code&gt;bash --noprofile --norc -eo pipefail&lt;/code&gt;), or your &lt;code&gt;shell:&lt;/code&gt; override.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;$GITHUB_WORKSPACE&lt;/code&gt;, &lt;code&gt;$CI&lt;/code&gt;, &lt;code&gt;$GITHUB_ACTIONS&lt;/code&gt;, per-step &lt;code&gt;working-directory&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What it doesn't (yet) — stated, not hidden
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;uses:&lt;/code&gt; steps&lt;/strong&gt; (marketplace actions like &lt;code&gt;actions/checkout&lt;/code&gt;, &lt;code&gt;actions/setup-node&lt;/code&gt;) are skipped with a note. Most are no-ops against your local checkout anyway. Running them with full Docker environment parity is the headline feature on the roadmap.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;${{ expressions }}&lt;/code&gt;&lt;/strong&gt; aren't evaluated — steps run with the literal env. &lt;code&gt;if:&lt;/code&gt; conditions and &lt;code&gt;matrix&lt;/code&gt; are shown, not enforced.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When walkflow hits one of these, it tells you. No silent surprises.&lt;/p&gt;

&lt;h2&gt;
  
  
  Handy flags
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;walkflow                       &lt;span class="c"&gt;# finds the single workflow under .github/workflows/&lt;/span&gt;
walkflow ci.yml &lt;span class="nt"&gt;--job&lt;/span&gt; build    &lt;span class="c"&gt;# pick a file and job&lt;/span&gt;
walkflow &lt;span class="nt"&gt;--list&lt;/span&gt;                &lt;span class="c"&gt;# print jobs and steps, then exit&lt;/span&gt;
walkflow &lt;span class="nt"&gt;--from&lt;/span&gt; 4              &lt;span class="c"&gt;# auto-run steps 1–3, go interactive from step 4&lt;/span&gt;
walkflow &lt;span class="nt"&gt;--from&lt;/span&gt; &lt;span class="s2"&gt;"run tests"&lt;/span&gt;    &lt;span class="c"&gt;# ...or select that step by name&lt;/span&gt;
walkflow &lt;span class="nt"&gt;-y&lt;/span&gt;                    &lt;span class="c"&gt;# run it all, no pausing — a fast local sanity check&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;--from&lt;/code&gt; is the one I reach for most: it fast-forwards through the steps you already trust (accumulating their env correctly) and hands you the controls exactly at the step you're actually debugging.&lt;/p&gt;

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

&lt;p&gt;It's a single Rust binary, no runtime, no daemon:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;cargo &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--git&lt;/span&gt; https://github.com/mrvlyouknowwho/walkflow
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;MIT licensed. Repo, issues, and roadmap are here: &lt;strong&gt;&lt;a href="https://github.com/mrvlyouknowwho/walkflow" rel="noopener noreferrer"&gt;github.com/mrvlyouknowwho/walkflow&lt;/a&gt;&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;If you live in GitHub Actions and you've ever pushed a commit whose entire message was &lt;code&gt;fix ci&lt;/code&gt; (then &lt;code&gt;fix ci again&lt;/code&gt;, then &lt;code&gt;pls&lt;/code&gt;), I'd genuinely like to know whether this shortens your loop. Issues and reactions welcome.&lt;/p&gt;

</description>
      <category>githubactions</category>
      <category>rust</category>
      <category>devops</category>
      <category>cli</category>
    </item>
  </channel>
</rss>
