<?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: Olivier Buitelaar</title>
    <description>The latest articles on DEV Community by Olivier Buitelaar (@ollieb89).</description>
    <link>https://dev.to/ollieb89</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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3139079%2F29ccfdba-1690-4059-a55c-3fbf7c36a4c8.jpeg</url>
      <title>DEV Community: Olivier Buitelaar</title>
      <link>https://dev.to/ollieb89</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/ollieb89"/>
    <language>en</language>
    <item>
      <title>MCP + Workflow Patterns: Orchestrating Complex CI Pipelines</title>
      <dc:creator>Olivier Buitelaar</dc:creator>
      <pubDate>Mon, 06 Apr 2026 10:44:21 +0000</pubDate>
      <link>https://dev.to/ollieb89/mcp-workflow-patterns-orchestrating-complex-ci-pipelines-17h1</link>
      <guid>https://dev.to/ollieb89/mcp-workflow-patterns-orchestrating-complex-ci-pipelines-17h1</guid>
      <description>&lt;p&gt;Last week, I shipped &lt;strong&gt;workflow-guardian&lt;/strong&gt; — a GitHub Action that lints your CI/CD workflow files. But the real story isn't just validation; it's about &lt;em&gt;orchestration patterns&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;If you read my previous post on &lt;a href="https://dev.to/ollieb89/how-openclaw-implements-mcp-for-multi-agent-orchestration-36hk"&gt;OpenClaw's MCP implementation&lt;/a&gt;, you know MCP shines when coordinating distributed agents. The same principles apply to GitHub Actions.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem: Complexity Explosion
&lt;/h2&gt;

&lt;p&gt;Modern CI/CD pipelines are complex. You have:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Matrix builds&lt;/strong&gt; (multiple OS, Node versions)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Conditional jobs&lt;/strong&gt; (run only on main branch, skip on docs)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Secrets &amp;amp; environments&lt;/strong&gt; (different for staging/prod)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Orchestration&lt;/strong&gt; (wait for tests before deploying)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This complexity leads to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Silent failures (job runs when it shouldn't)&lt;/li&gt;
&lt;li&gt;Race conditions (deploy before tests finish)&lt;/li&gt;
&lt;li&gt;Security gaps (hardcoded secrets)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The Pattern: MCP-Inspired Constraints
&lt;/h2&gt;

&lt;p&gt;Think of your GitHub Actions workflow as a &lt;strong&gt;distributed system&lt;/strong&gt;. Each job is an agent. Jobs need:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Contract validation&lt;/strong&gt; — Declare what each job expects&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Message passing&lt;/strong&gt; — Jobs communicate via outputs&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Constraint enforcement&lt;/strong&gt; — Prevent invalid states&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This is &lt;em&gt;exactly&lt;/em&gt; what &lt;strong&gt;workflow-guardian&lt;/strong&gt; does.&lt;/p&gt;

&lt;h2&gt;
  
  
  Real Win: Catch Bugs Before They Ship
&lt;/h2&gt;

&lt;p&gt;In production systems, we use this pattern. A colleague accidentally created a circular dependency in a deploy workflow. &lt;strong&gt;workflow-guardian caught it before merge.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The cost of catching that in CI vs. production: hours vs. incidents.&lt;/p&gt;

&lt;h2&gt;
  
  
  Next Steps
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Add &lt;strong&gt;workflow-guardian&lt;/strong&gt; to your repos: &lt;code&gt;uses: ollieb89/workflow-guardian@v1&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Start writing &lt;strong&gt;job contracts&lt;/strong&gt; (explicit outputs)&lt;/li&gt;
&lt;li&gt;Use &lt;strong&gt;conditional orchestration&lt;/strong&gt; to enforce safe deployment sequences&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Questions?&lt;/strong&gt; Drop a comment.&lt;/p&gt;

</description>
      <category>github</category>
      <category>ci</category>
      <category>orchestration</category>
      <category>mcp</category>
    </item>
    <item>
      <title>How OpenClaw Implements MCP for Multi-Agent Orchestration</title>
      <dc:creator>Olivier Buitelaar</dc:creator>
      <pubDate>Tue, 24 Mar 2026 17:57:28 +0000</pubDate>
      <link>https://dev.to/ollieb89/how-openclaw-implements-mcp-for-multi-agent-orchestration-36hk</link>
      <guid>https://dev.to/ollieb89/how-openclaw-implements-mcp-for-multi-agent-orchestration-36hk</guid>
      <description>&lt;h1&gt;
  
  
  How OpenClaw Implements MCP for Multi-Agent Orchestration
&lt;/h1&gt;

&lt;p&gt;When you search for MCP orchestration today, the top results are Google ADK and Dynatrace — both solid tools for their respective niches. But they tell only part of the story. Google ADK is purpose-built for Google Cloud environments. Dynatrace approaches MCP from an observability angle. Neither gives you a self-hosted, multi-channel, multi-agent orchestration framework that treats MCP servers as native first-class tools with zero glue code.&lt;/p&gt;

&lt;p&gt;That's what OpenClaw is.&lt;/p&gt;

&lt;p&gt;OpenClaw is an open-source AI agent framework built around the idea that Model Context Protocol tools should just &lt;em&gt;work&lt;/em&gt; — inside any agent, across any channel (Telegram, Discord, WhatsApp, Slack), without custom integration code. This guide walks through exactly how OpenClaw implements multi-agent MCP: the architecture, the practical setup, and how it compares to the alternatives.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Is MCP (and Why It Matters for Multi-Agent Systems)?
&lt;/h2&gt;

&lt;p&gt;Model Context Protocol (MCP) is an open standard that defines how AI agents discover and call external tools and data sources. It was introduced to solve the fragmentation problem: every tool integration used to require bespoke code. MCP standardises that surface.&lt;/p&gt;

&lt;p&gt;MCP has three core primitives:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Tools&lt;/strong&gt; — callable functions that agents invoke to take action (search the web, send an email, run a query)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Resources&lt;/strong&gt; — structured data exposed by the server (files, database records, API responses)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Prompts&lt;/strong&gt; — reusable prompt templates with parameterised inputs&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For a multi-agent system, these primitives matter because they establish a &lt;em&gt;shared tool interface&lt;/em&gt;. When every capability is described via MCP's self-describing schemas, agents can discover what's available, understand input/output contracts, and call tools without hardcoded bindings. You get plug-and-play capability composition across agents.&lt;/p&gt;

&lt;p&gt;The problem is that most implementations stop here — they wire up a single agent to a single MCP server. Production multi-agent MCP orchestration requires more: routing tools to the right agents, managing sessions, controlling access, and coordinating across channels. That's the gap OpenClaw fills.&lt;/p&gt;




&lt;h2&gt;
  
  
  OpenClaw's MCP Architecture: Three Levels
&lt;/h2&gt;

&lt;p&gt;OpenClaw integrates MCP at three distinct levels, each serving a different use case. Together they cover the full spectrum from broad app integrations to targeted browser automation.&lt;/p&gt;

&lt;h3&gt;
  
  
  Level 1: Plugin-Level MCP Clients
&lt;/h3&gt;

&lt;p&gt;The primary integration point is OpenClaw's plugin system. Plugins are npm packages that connect to MCP servers at startup and register their tools as native agent tools inside the OpenClaw gateway. From the agent's perspective, an MCP tool looks identical to a built-in tool — no distinction, no extra configuration per call.&lt;/p&gt;

&lt;p&gt;The Composio plugin is the canonical example. It connects to &lt;code&gt;https://connect.composio.dev/mcp&lt;/code&gt; and registers 500+ app integrations — Gmail, Slack, GitHub, Notion, Google Workspace, and more — directly into the agent tool registry.&lt;/p&gt;

&lt;p&gt;Plugin configuration lives in &lt;code&gt;openclaw.json&lt;/code&gt; under &lt;code&gt;plugins.entries&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"plugins"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"entries"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"package"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"@composio/openclaw-plugin"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"config"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"apiKey"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"${COMPOSIO_API_KEY}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"mcpEndpoint"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://connect.composio.dev/mcp"&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice the &lt;code&gt;${COMPOSIO_API_KEY}&lt;/code&gt; syntax — OpenClaw interpolates environment variables directly in config, keeping secrets out of version control.&lt;/p&gt;

&lt;p&gt;When the gateway starts, the plugin connects to the MCP server, fetches the tool manifest, and registers each tool. Agents can then call &lt;code&gt;GMAIL_SEND_EMAIL&lt;/code&gt; or &lt;code&gt;GITHUB_CREATE_ISSUE&lt;/code&gt; exactly as they would any native capability.&lt;/p&gt;

&lt;h3&gt;
  
  
  Level 2: Skill-Level MCP via mcporter
&lt;/h3&gt;

&lt;p&gt;Not every MCP integration warrants a full plugin. For direct, CLI-level access to any MCP server, OpenClaw includes &lt;code&gt;mcporter&lt;/code&gt; — a built-in skill that exposes MCP servers as command-line tools.&lt;/p&gt;

&lt;p&gt;Core mcporter commands:&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;# List all tools available on a connected MCP server&lt;/span&gt;
mcporter list

&lt;span class="c"&gt;# Call a specific tool with key=value arguments&lt;/span&gt;
mcporter call server.tool_name &lt;span class="nv"&gt;key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;value &lt;span class="nv"&gt;key2&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;value2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;mcporter supports all three MCP transport protocols:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Stdio&lt;/strong&gt; — for local MCP servers spawned as subprocesses&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SSE (Server-Sent Events)&lt;/strong&gt; — for remote HTTP-based MCP servers&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;WebSocket&lt;/strong&gt; — for persistent bidirectional connections&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This makes mcporter the right tool when you want to test an MCP server before writing a plugin, script one-off MCP calls, or integrate a niche server that doesn't have a dedicated OpenClaw plugin yet.&lt;/p&gt;

&lt;h3&gt;
  
  
  Level 3: Browser MCP Integration
&lt;/h3&gt;

&lt;p&gt;OpenClaw ships with Chrome DevTools MCP integration for browser automation. Activate it with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;openclaw browser &lt;span class="nt"&gt;--mode&lt;/span&gt; user
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This connects the Chrome DevTools Protocol via MCP, giving agents the ability to automate browser interactions — navigation, form filling, screenshot capture, DOM inspection — through the same MCP tool interface used everywhere else.&lt;/p&gt;




&lt;h2&gt;
  
  
  Practical Walkthrough: Adding MCP Tools to Your Agents
&lt;/h2&gt;

&lt;p&gt;Here's the end-to-end flow for wiring a new MCP server into your OpenClaw setup.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: Install the Plugin
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;openclaw plugins &lt;span class="nb"&gt;install&lt;/span&gt; @scope/your-mcp-plugin
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;OpenClaw validates the plugin manifest and runs install with &lt;code&gt;--ignore-scripts&lt;/code&gt; by default for security. You can override this explicitly if needed, but the safe default prevents supply-chain script injection.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2: Configure in openclaw.json
&lt;/h3&gt;

&lt;p&gt;Add the plugin entry with any required config. Use &lt;code&gt;${ENV_VAR}&lt;/code&gt; for secrets:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"plugins"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"entries"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"package"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"@scope/your-mcp-plugin"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"config"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"endpoint"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://your-mcp-server.example.com"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"apiKey"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"${YOUR_API_KEY}"&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 3: Control Per-Agent Tool Access
&lt;/h3&gt;

&lt;p&gt;This is where OpenClaw's multi-agent MCP orchestration shines. You don't have to expose every MCP tool to every agent. Use allowlists in &lt;code&gt;agents.list[].tools.allow&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"agents"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"list"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"content-agent"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"tools"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"allow"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"GMAIL_SEND_EMAIL"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"NOTION_CREATE_PAGE"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"GITHUB_CREATE_ISSUE"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ops-agent"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"tools"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"allow"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"SLACK_SEND_MESSAGE"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"GITHUB_LIST_ISSUES"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"GITHUB_UPDATE_ISSUE"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each agent only sees — and can only call — the tools explicitly allowed for it. This is how you implement least-privilege access across a multi-agent system without per-agent plugin configuration.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 4: Use — Transparently
&lt;/h3&gt;

&lt;p&gt;Once configured, agents call MCP tools exactly like any other capability. There's no special syntax, no MCP-specific client code in your agent logic. The OpenClaw gateway handles the MCP protocol layer entirely.&lt;/p&gt;

&lt;p&gt;For direct CLI access, use mcporter:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;mcporter call gmail.send_email &lt;span class="nv"&gt;to&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"team@example.com"&lt;/span&gt; &lt;span class="nv"&gt;subject&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"Deploy complete"&lt;/span&gt; &lt;span class="nv"&gt;body&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"v2.1.0 is live."&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  OpenClaw vs Generic MCP Client
&lt;/h2&gt;

&lt;p&gt;If you're evaluating whether to use OpenClaw or wire up your own MCP client directly, here's the honest comparison:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Capability&lt;/th&gt;
&lt;th&gt;Generic MCP Client&lt;/th&gt;
&lt;th&gt;OpenClaw&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;MCP tool execution&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Multi-agent tool routing&lt;/td&gt;
&lt;td&gt;Manual&lt;/td&gt;
&lt;td&gt;Built-in allowlists&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Persistent gateway daemon&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Always-on session management&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Multi-channel delivery&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Telegram, Discord, WhatsApp, Slack&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hierarchical orchestration&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Parent/child session model&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Plugin security&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Manifest validation + ignore-scripts&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Scheduled MCP tool runs&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Cron integration&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tool sandboxing&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Configurable exec security&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;IDE integration (ACP)&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;ACP bridge for Codex, Claude Code&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A generic MCP client gives you the protocol. OpenClaw gives you the orchestration layer built on top of it.&lt;/p&gt;




&lt;h2&gt;
  
  
  How OpenClaw Compares to Google ADK and Dynatrace
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Google ADK
&lt;/h3&gt;

&lt;p&gt;Google's Agent Development Kit has solid MCP support and integrates cleanly with the Google Cloud ecosystem — Vertex AI, Cloud Run, BigQuery. If you're building agents that live entirely within GCP, ADK is a reasonable choice.&lt;/p&gt;

&lt;p&gt;The limitation is vendor lock-in. ADK assumes Google Cloud infrastructure. You can't run it locally on a laptop with your own gateway daemon, route messages through Telegram, or manage a mixed fleet of agents across channels without significant custom work. OpenClaw runs wherever Node.js runs — local, VPS, or cloud — with no infrastructure dependency.&lt;/p&gt;

&lt;h3&gt;
  
  
  Dynatrace
&lt;/h3&gt;

&lt;p&gt;Dynatrace's MCP integration is primarily observability-oriented. Their angle is using MCP to let AI agents query monitoring data, trigger workflows, and surface insights from their platform. It's genuinely useful if you're already deep in the Dynatrace ecosystem and want AI-driven ops.&lt;/p&gt;

&lt;p&gt;But Dynatrace is not an orchestration framework. It doesn't manage agent sessions, route tools across agents, or provide a programmable multi-agent backbone. OpenClaw approaches the same MCP standard from the orchestration angle — the question isn't "how do I query my observability platform via MCP?" but "how do I wire 500 different tools into a production multi-agent system and control which agent can call what?"&lt;/p&gt;

&lt;p&gt;Different problems. OpenClaw solves the orchestration one.&lt;/p&gt;




&lt;h2&gt;
  
  
  Building Your Own MCP Server for OpenClaw
&lt;/h2&gt;

&lt;p&gt;The MCP ecosystem grows through community-built servers. If you have a service or capability you want to expose to OpenClaw agents, here's the structure:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;my-mcp-server/
├── package.json
├── src/
│   ├── index.ts          # MCP server entry point
│   ├── tools/            # Tool definitions
│   │   ├── create.ts
│   │   ├── read.ts
│   │   └── update.ts
│   └── resources/        # Resource definitions (optional)
└── README.md
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Design principles for OpenClaw-compatible MCP servers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Action-oriented names: create_issue, send_message, fetch_record — not vague like process or handle&lt;/li&gt;
&lt;li&gt;Single purpose per tool: one tool does one thing; composition happens at the agent layer&lt;/li&gt;
&lt;li&gt;Complete schemas: every input and output field described; agents use these to reason about tool usage&lt;/li&gt;
&lt;li&gt;Structured output: return JSON objects, not unstructured strings; agents parse structured responses reliably&lt;/li&gt;
&lt;li&gt;Env-based config: secrets via environment variables, never hardcoded; document required vars in README&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Publish via npm. Once published, your server is discoverable through ClawHub (available as of v2026.3.22), OpenClaw's community registry for plugins and MCP servers.&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;# Install your published MCP server as an OpenClaw plugin&lt;/span&gt;
openclaw plugins &lt;span class="nb"&gt;install &lt;/span&gt;my-mcp-server
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Getting Started
&lt;/h2&gt;

&lt;p&gt;If you want OpenClaw MCP orchestration running today:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Install OpenClaw&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;npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; openclaw
openclaw init
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;2. Enable the Composio plugin&lt;/strong&gt; for 500+ app integrations out of the box:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;openclaw plugins &lt;span class="nb"&gt;install&lt;/span&gt; @composio/openclaw-plugin
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then add your COMPOSIO_API_KEY to your environment and configure the plugin entry in openclaw.json.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Enable mcporter&lt;/strong&gt; for direct CLI MCP access — it's a built-in skill, enabled by default. Run &lt;code&gt;mcporter list&lt;/code&gt; after connecting a server to see available tools.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Build your own MCP server&lt;/strong&gt; — follow the structure above, publish to npm, and list it on ClawHub.&lt;/p&gt;

&lt;p&gt;Resources:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;GitHub: &lt;a href="https://github.com/openclaw/openclaw" rel="noopener noreferrer"&gt;https://github.com/openclaw/openclaw&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Docs: &lt;a href="https://docs.openclaw.ai" rel="noopener noreferrer"&gt;https://docs.openclaw.ai&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Discord: &lt;a href="https://discord.com/invite/clawd" rel="noopener noreferrer"&gt;https://discord.com/invite/clawd&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;ClawHub: &lt;a href="https://clawhub.com" rel="noopener noreferrer"&gt;https://clawhub.com&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The multi-agent MCP future isn't locked to any cloud vendor. It runs wherever your agents run.&lt;/p&gt;




</description>
      <category>ai</category>
      <category>mcp</category>
      <category>programming</category>
      <category>webdev</category>
    </item>
    <item>
      <title>`pull_request_target` Without Regret: Secure Fork PRs in GitHub Actions</title>
      <dc:creator>Olivier Buitelaar</dc:creator>
      <pubDate>Sat, 21 Mar 2026 19:02:35 +0000</pubDate>
      <link>https://dev.to/ollieb89/pullrequesttarget-without-regret-secure-fork-prs-in-github-actions-1jpi</link>
      <guid>https://dev.to/ollieb89/pullrequesttarget-without-regret-secure-fork-prs-in-github-actions-1jpi</guid>
      <description>&lt;h1&gt;
  
  
  &lt;code&gt;pull_request_target&lt;/code&gt; Without Regret: Secure Fork PRs in GitHub Actions
&lt;/h1&gt;

&lt;p&gt;If you maintain a public repo, you eventually hit this tradeoff:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You want CI + automation on contributions from forks.&lt;/li&gt;
&lt;li&gt;You &lt;em&gt;don’t&lt;/em&gt; want to leak secrets or run untrusted code with elevated permissions.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A lot of teams switch to &lt;code&gt;pull_request_target&lt;/code&gt; to get access to secrets and write permissions (for labels/comments), then accidentally check out and execute fork code in the same job. That’s one of the fastest ways to create a supply-chain incident in your own repo.&lt;/p&gt;

&lt;p&gt;In this post, I’ll show a safer pattern I use in real repos:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Split &lt;strong&gt;untrusted validation&lt;/strong&gt; from &lt;strong&gt;trusted automation&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Avoid checking out attacker-controlled code in privileged contexts.&lt;/li&gt;
&lt;li&gt;Use explicit permissions and OIDC (where relevant).&lt;/li&gt;
&lt;li&gt;Add guardrails so regressions are caught automatically.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Why &lt;code&gt;pull_request_target&lt;/code&gt; is risky
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;pull_request_target&lt;/code&gt; runs in the context of the &lt;strong&gt;base repository&lt;/strong&gt;, not the fork. That means it can access repo secrets and can get elevated &lt;code&gt;GITHUB_TOKEN&lt;/code&gt; permissions.&lt;/p&gt;

&lt;p&gt;That’s useful for trusted tasks like labeling, but dangerous if your workflow does this:&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;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;pull_request_target&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;types&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;opened&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;synchronize&lt;/span&gt;&lt;span class="pi"&gt;]&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;bad-example&lt;/span&gt;&lt;span class="pi"&gt;:&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;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="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;ref&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ github.event.pull_request.head.sha }}&lt;/span&gt;
      &lt;span class="pi"&gt;-&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;npm ci &amp;amp;&amp;amp; npm test&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This checks out untrusted fork code and executes it &lt;em&gt;inside a privileged event context&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Even if you think “it’s just tests,” test scripts can run arbitrary shell commands, exfiltrate tokens, or abuse write permissions.&lt;/p&gt;




&lt;h2&gt;
  
  
  The safer architecture: two workflows
&lt;/h2&gt;

&lt;p&gt;Use:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;pull_request&lt;/code&gt; for &lt;strong&gt;untrusted code execution&lt;/strong&gt; (no secrets, minimal token permissions)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;pull_request_target&lt;/code&gt; for &lt;strong&gt;trusted metadata actions only&lt;/strong&gt; (labels, comments, triage)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Workflow A: untrusted CI (&lt;code&gt;pull_request&lt;/code&gt;)
&lt;/h3&gt;

&lt;p&gt;This workflow can build/test fork code, but should have minimal permissions and no secrets.&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="c1"&gt;# .github/workflows/pr-ci.yml&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;PR CI (untrusted)&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;pull_request&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;types&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;opened&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;synchronize&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;reopened&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;

&lt;span class="na"&gt;permissions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;contents&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;read&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;test&lt;/span&gt;&lt;span class="pi"&gt;:&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 PR code&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;Set up Node&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-node@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;node-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;20'&lt;/span&gt;
          &lt;span class="na"&gt;cache&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npm&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;Install deps&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;npm ci&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;Run tests&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;npm test -- --ci&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Key point: this is where untrusted code runs.&lt;/p&gt;

&lt;h3&gt;
  
  
  Workflow B: trusted repo automation (&lt;code&gt;pull_request_target&lt;/code&gt;)
&lt;/h3&gt;

&lt;p&gt;This workflow should not execute PR code. It should work only with event metadata.&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="c1"&gt;# .github/workflows/pr-triage.yml&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;PR Triage (trusted)&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;pull_request_target&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;types&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;opened&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;reopened&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;

&lt;span class="na"&gt;permissions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;contents&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;read&lt;/span&gt;
  &lt;span class="na"&gt;pull-requests&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;write&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;label-size&lt;/span&gt;&lt;span class="pi"&gt;:&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;Add size label based on files changed&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;ollieb89/pr-size-labeler@v1&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;github_token&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.GITHUB_TOKEN }}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No checkout of &lt;code&gt;head.sha&lt;/code&gt;, no running contributor scripts, no package install from the PR branch.&lt;/p&gt;




&lt;h2&gt;
  
  
  If you must run privileged follow-up after CI
&lt;/h2&gt;

&lt;p&gt;Sometimes you need a trusted action &lt;em&gt;after&lt;/em&gt; untrusted checks pass (for example, posting a summary comment or syncing metadata). Use &lt;code&gt;workflow_run&lt;/code&gt; as a boundary.&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="c1"&gt;# .github/workflows/pr-post-ci.yml&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;PR Post-CI (trusted follow-up)&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_run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;workflows&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;PR&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;CI&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;(untrusted)"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;types&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;completed&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;

&lt;span class="na"&gt;permissions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;contents&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;read&lt;/span&gt;
  &lt;span class="na"&gt;pull-requests&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;write&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;comment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;&amp;gt;-&lt;/span&gt;
      &lt;span class="s"&gt;github.event.workflow_run.conclusion == 'success'&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;Comment on PR safely&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/github-script@v7&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;script&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
            &lt;span class="s"&gt;const run = context.payload.workflow_run;&lt;/span&gt;
            &lt;span class="s"&gt;const prs = run.pull_requests || [];&lt;/span&gt;
            &lt;span class="s"&gt;if (!prs.length) return;&lt;/span&gt;
            &lt;span class="s"&gt;await github.rest.issues.createComment({&lt;/span&gt;
              &lt;span class="s"&gt;owner: context.repo.owner,&lt;/span&gt;
              &lt;span class="s"&gt;repo: context.repo.repo,&lt;/span&gt;
              &lt;span class="s"&gt;issue_number: prs[0].number,&lt;/span&gt;
              &lt;span class="s"&gt;body: '✅ CI passed. Maintainers can review safely.'&lt;/span&gt;
            &lt;span class="s"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This avoids executing untrusted code in the trusted phase.&lt;/p&gt;




&lt;h2&gt;
  
  
  Permission hardening checklist
&lt;/h2&gt;

&lt;p&gt;Even with split workflows, tighten defaults:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Set explicit &lt;code&gt;permissions&lt;/code&gt; in every workflow&lt;/strong&gt; (don’t rely on defaults).&lt;/li&gt;
&lt;li&gt;Prefer &lt;code&gt;contents: read&lt;/code&gt; and add write scopes only when required.&lt;/li&gt;
&lt;li&gt;Avoid long-lived cloud keys in secrets; prefer OIDC short-lived credentials.&lt;/li&gt;
&lt;li&gt;Pin third-party actions to a commit SHA where possible.&lt;/li&gt;
&lt;li&gt;Restrict &lt;code&gt;pull_request_target&lt;/code&gt; workflows to metadata operations.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Example of pinning:&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="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&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@b4ffde65f46336ab88eb53be808477a3936bae11&lt;/span&gt; &lt;span class="c1"&gt;# v4.1.1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Catch bad patterns automatically
&lt;/h2&gt;

&lt;p&gt;This is where linting/policy checks help. You want CI to fail if someone introduces dangerous patterns like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;pull_request_target&lt;/code&gt; + checkout of &lt;code&gt;github.event.pull_request.head.sha&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;broad write permissions in untrusted workflows&lt;/li&gt;
&lt;li&gt;unpinned external actions&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A practical approach is to add a dedicated workflow policy step.&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;Workflow Policy&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;pull_request&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;paths&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;.github/workflows/**'&lt;/span&gt;

&lt;span class="na"&gt;permissions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;contents&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;read&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;policy&lt;/span&gt;&lt;span class="pi"&gt;:&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;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;Validate workflow security&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;ollieb89/workflow-guardian@v1&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;fail_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;high&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If your org has custom requirements, encode them once and enforce them on every PR instead of relying on manual review memory.&lt;/p&gt;




&lt;h2&gt;
  
  
  Common “almost safe” mistakes
&lt;/h2&gt;

&lt;p&gt;I still see these in otherwise good repos:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Using &lt;code&gt;pull_request_target&lt;/code&gt; for tests&lt;/strong&gt; because “we needed secrets quickly.”&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Checking out &lt;code&gt;head.sha&lt;/code&gt; in trusted workflows&lt;/strong&gt; to parse files.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Default token permissions&lt;/strong&gt; left too broad.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Comment bots with repo write access&lt;/strong&gt; that also run shell commands from PR input.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you recognize one of these, the fix is usually architectural, not just another &lt;code&gt;if:&lt;/code&gt; condition.&lt;/p&gt;




&lt;h2&gt;
  
  
  A practical baseline you can adopt today
&lt;/h2&gt;

&lt;p&gt;If you only do three things this week, do these:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Move all fork code execution to &lt;code&gt;pull_request&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Keep &lt;code&gt;pull_request_target&lt;/code&gt; metadata-only.&lt;/li&gt;
&lt;li&gt;Add a workflow policy check to prevent regressions.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That gives you a strong baseline without slowing contributor velocity.&lt;/p&gt;




&lt;h2&gt;
  
  
  Toolkit links
&lt;/h2&gt;

&lt;p&gt;If you want to implement this pattern quickly, these are the actions I use:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;workflow-guardian&lt;/strong&gt; (workflow policy/security checks): &lt;a href="https://github.com/marketplace/actions/workflow-guardian" rel="noopener noreferrer"&gt;https://github.com/marketplace/actions/workflow-guardian&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;pr-size-labeler&lt;/strong&gt; (safe PR size labeling): &lt;a href="https://github.com/ollieb89/pr-size-labeler" rel="noopener noreferrer"&gt;https://github.com/ollieb89/pr-size-labeler&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;stale-branch-cleaner&lt;/strong&gt;: &lt;a href="https://github.com/ollieb89/stale-branch-cleaner" rel="noopener noreferrer"&gt;https://github.com/ollieb89/stale-branch-cleaner&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;changelog-generator&lt;/strong&gt;: &lt;a href="https://github.com/ollieb89/changelog-generator" rel="noopener noreferrer"&gt;https://github.com/ollieb89/changelog-generator&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;test-results-reporter&lt;/strong&gt;: &lt;a href="https://github.com/ollieb89/test-results-reporter" rel="noopener noreferrer"&gt;https://github.com/ollieb89/test-results-reporter&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you’re maintaining public repos, treat workflow design like production code. The event model and permission boundaries matter as much as the scripts themselves.&lt;/p&gt;

</description>
      <category>githubactions</category>
      <category>security</category>
      <category>devops</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Stop Ghosting Your Own Repos: Automate Stale Branch Cleanup with GitHub Actions</title>
      <dc:creator>Olivier Buitelaar</dc:creator>
      <pubDate>Sat, 21 Mar 2026 13:02:07 +0000</pubDate>
      <link>https://dev.to/ollieb89/stop-ghosting-your-own-repos-automate-stale-branch-cleanup-with-github-actions-5ep3</link>
      <guid>https://dev.to/ollieb89/stop-ghosting-your-own-repos-automate-stale-branch-cleanup-with-github-actions-5ep3</guid>
      <description>&lt;h1&gt;
  
  
  Stop Ghosting Your Own Repos: Automate Stale Branch Cleanup with GitHub Actions
&lt;/h1&gt;

&lt;p&gt;You know the feeling. You run &lt;code&gt;git branch -a&lt;/code&gt; and a wall of text scrolls past. Most of those branches are "temporary" fixes from 2024. Some are features that were merged months ago but never deleted. A few are experiments that died a quiet death.&lt;/p&gt;

&lt;p&gt;Leaving stale branches lying around isn't just "messy." It slows down your team, makes it harder to find what's actually active, and creates a mental tax every time you look at your repository.&lt;/p&gt;

&lt;p&gt;In this guide, I'll show you how to automate the audit and cleanup of stale branches using GitHub Actions, so you never have to manually "clean house" again.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem with Manual Cleanup
&lt;/h2&gt;

&lt;p&gt;Most developers treat branch cleanup like doing the dishes: they wait until the pile is so high they can't ignore it anymore. Then someone spends an hour guessing which branches are safe to delete, occasionally deleting something important by mistake, and inevitably missing half the mess.&lt;/p&gt;

&lt;p&gt;The issues with manual cleanup are clear:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;It's inconsistent.&lt;/strong&gt; It only happens when someone gets annoyed enough.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It's risky.&lt;/strong&gt; Without a clear record of when a branch was last touched, you're guessing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It's boring.&lt;/strong&gt; No one wants to spend their Friday afternoon running &lt;code&gt;git branch -D&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Automating the Audit
&lt;/h2&gt;

&lt;p&gt;The first step in a safe cleanup process isn't deletion—it's visibility. You need to know which branches are stale &lt;em&gt;before&lt;/em&gt; they disappear.&lt;/p&gt;

&lt;p&gt;We can use a GitHub Action to scan the repository for branches that haven't seen a commit in a specific number of days (e.g., 90 days). Instead of deleting them immediately, the action should create a report.&lt;/p&gt;

&lt;p&gt;Here is a basic implementation strategy using a custom GitHub Action I built called &lt;code&gt;stale-branch-cleaner&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: Create the Cleanup Workflow
&lt;/h3&gt;

&lt;p&gt;Create a file at &lt;code&gt;.github/workflows/stale-branches.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;Stale Branch Audit&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;schedule&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;cron&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;0&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;9&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;1'&lt;/span&gt;  &lt;span class="c1"&gt;# Run every Monday at 9:00 AM&lt;/span&gt;
  &lt;span class="na"&gt;workflow_dispatch&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;      &lt;span class="c1"&gt;# Allow manual triggering&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;audit&lt;/span&gt;&lt;span class="pi"&gt;:&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;permissions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;contents&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;write&lt;/span&gt;
      &lt;span class="na"&gt;issues&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;write&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;Check for stale branches&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;ollieb89/stale-branch-cleaner@v1&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;stale-days&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;90'&lt;/span&gt;
          &lt;span class="na"&gt;dry-run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;true'&lt;/span&gt;      &lt;span class="c1"&gt;# Important: report only, don't delete yet&lt;/span&gt;
          &lt;span class="na"&gt;create-issue&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;true'&lt;/span&gt;  &lt;span class="c1"&gt;# Create a GitHub Issue with the results&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 2: Review the Report
&lt;/h3&gt;

&lt;p&gt;When this workflow runs, it won't touch your code. Instead, it will open a GitHub Issue that looks like this:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Title:&lt;/strong&gt; 🧹 Stale Branch Report (2026-03-21)&lt;br&gt;
&lt;strong&gt;Body:&lt;/strong&gt;&lt;br&gt;
The following branches have had no activity for over 90 days:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Branch&lt;/th&gt;
&lt;th&gt;Last Commit&lt;/th&gt;
&lt;th&gt;Author&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;feature/old-ui-experiment&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;2024-11-12&lt;/td&gt;
&lt;td&gt;@dev-alpha&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;fix/temp-patch-v1&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;2025-01-05&lt;/td&gt;
&lt;td&gt;@dev-beta&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;refactor/removed-module&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;2024-09-30&lt;/td&gt;
&lt;td&gt;@dev-alpha&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Total stale branches found: 3.&lt;/p&gt;
&lt;h2&gt;
  
  
  Moving to Auto-Deletion
&lt;/h2&gt;

&lt;p&gt;Once you've run the audit for a few weeks and you're confident in the results, you can move to a "soft delete" or a full auto-cleanup.&lt;/p&gt;
&lt;h3&gt;
  
  
  The "Safe" Auto-Delete Pattern
&lt;/h3&gt;

&lt;p&gt;I recommend a 90-day stale window with a "dry run" period of one month. This gives everyone on the team a chance to see the issue and "rescue" any branch they still care about (by simply pushing a dummy commit or re-labeling it).&lt;/p&gt;

&lt;p&gt;To enable auto-deletion, update your workflow:&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="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;Cleanup stale branches&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;ollieb89/stale-branch-cleaner@v1&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;stale-days&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;120'&lt;/span&gt;    &lt;span class="c1"&gt;# Older branches only&lt;/span&gt;
    &lt;span class="na"&gt;dry-run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;false'&lt;/span&gt;     &lt;span class="c1"&gt;# This will actually delete branches!&lt;/span&gt;
    &lt;span class="na"&gt;create-issue&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;true'&lt;/span&gt; &lt;span class="c1"&gt;# Still create an issue to log what was removed&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Protecting Important Branches
&lt;/h3&gt;

&lt;p&gt;Not all "inactive" branches are stale. You might have long-lived &lt;code&gt;release&lt;/code&gt; branches, &lt;code&gt;hotfix&lt;/code&gt; branches, or documentation branches that shouldn't be touched even if they haven't moved in 6 months.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;stale-branch-cleaner&lt;/code&gt; action automatically protects:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;main&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;master&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;develop&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Any branch matching &lt;code&gt;release/*&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Any branch matching &lt;code&gt;hotfix/*&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can also provide a custom ignore list:&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;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;stale-days&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;90'&lt;/span&gt;
  &lt;span class="na"&gt;ignore-branches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;archive/*,legacy-v1,important-refactor'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Why This Matters for DevOps
&lt;/h2&gt;

&lt;p&gt;Clean repositories lead to clean CI/CD. &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Faster clones:&lt;/strong&gt; Fewer refs to fetch when running shallow clones.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Clearer visibility:&lt;/strong&gt; Your branch dropdown only shows work that is actually happening.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lower security risk:&lt;/strong&gt; Old branches often contain outdated dependencies with known vulnerabilities that might accidentally get merged or deployed.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Stop letting "temporary" branches become permanent fixtures of your repository. Automating the cleanup process turns a manual chore into a background process that keeps your development environment sharp and professional.&lt;/p&gt;

&lt;p&gt;If you want to try this out, the tool is open source and available on GitHub:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/ollieb89/stale-branch-cleaner" rel="noopener noreferrer"&gt;stale-branch-cleaner&lt;/a&gt;&lt;/strong&gt; — Automates the audit and deletion of stale branches.&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  Part of the GitHub Actions Toolkit
&lt;/h3&gt;

&lt;p&gt;I'm building a suite of tools to make GitHub Actions more manageable for developers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/marketplace/actions/workflow-guardian" rel="noopener noreferrer"&gt;workflow-guardian&lt;/a&gt;&lt;/strong&gt; — Lints your workflows for security and best practices.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/ollieb89/test-results-reporter" rel="noopener noreferrer"&gt;test-results-reporter&lt;/a&gt;&lt;/strong&gt; — Aggregates all your test results into a single PR comment.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/ollieb89/pr-size-labeler" rel="noopener noreferrer"&gt;pr-size-labeler&lt;/a&gt;&lt;/strong&gt; — Auto-labels PRs by size to improve review quality.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Give them a star if they help your workflow!&lt;/p&gt;

</description>
      <category>githubactions</category>
      <category>devops</category>
      <category>productivity</category>
      <category>opensource</category>
    </item>
    <item>
      <title>I launched 3 small products to make CI debugging less chaotic</title>
      <dc:creator>Olivier Buitelaar</dc:creator>
      <pubDate>Sat, 21 Mar 2026 05:11:09 +0000</pubDate>
      <link>https://dev.to/ollieb89/i-launched-3-small-products-to-make-ci-debugging-less-chaotic-bbg</link>
      <guid>https://dev.to/ollieb89/i-launched-3-small-products-to-make-ci-debugging-less-chaotic-bbg</guid>
      <description>&lt;p&gt;If you work with GitHub Actions long enough, you start seeing the same pattern:&lt;/p&gt;

&lt;p&gt;A workflow fails.&lt;br&gt;
Someone opens the logs.&lt;br&gt;
A few guesses get tested.&lt;br&gt;
The job gets rerun.&lt;br&gt;
Eventually it goes green.&lt;br&gt;
And almost nobody writes down the real root cause clearly.&lt;/p&gt;

&lt;p&gt;That means teams lose time twice:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;when the workflow breaks&lt;/li&gt;
&lt;li&gt;when the same class of failure comes back later&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;So I made 3 small products to make CI debugging more structured.&lt;/p&gt;

&lt;h2&gt;
  
  
  1) GitHub Actions Triage Checklist
&lt;/h2&gt;

&lt;p&gt;This is a practical checklist for diagnosing failed GitHub Actions runs faster.&lt;/p&gt;

&lt;p&gt;It walks through common failure categories like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;triggers&lt;/li&gt;
&lt;li&gt;branch and path filters&lt;/li&gt;
&lt;li&gt;permissions&lt;/li&gt;
&lt;li&gt;secrets and tokens&lt;/li&gt;
&lt;li&gt;dependency issues&lt;/li&gt;
&lt;li&gt;cache problems&lt;/li&gt;
&lt;li&gt;runner issues&lt;/li&gt;
&lt;li&gt;action version changes&lt;/li&gt;
&lt;li&gt;concurrency conflicts&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The goal is simple: stop guessing and work through the likely causes in a repeatable order.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Link:&lt;/strong&gt;&lt;br&gt;
&lt;a href="https://trivexia.gumroad.com/l/github-actions-triage-checklist" rel="noopener noreferrer"&gt;GitHub Actions Triage Checklist&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  2) CI Debugging Template
&lt;/h2&gt;

&lt;p&gt;This is a lightweight template for documenting CI failures properly.&lt;/p&gt;

&lt;p&gt;It helps you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;capture what failed&lt;/li&gt;
&lt;li&gt;record the exact error&lt;/li&gt;
&lt;li&gt;test hypotheses&lt;/li&gt;
&lt;li&gt;confirm root cause&lt;/li&gt;
&lt;li&gt;document the fix&lt;/li&gt;
&lt;li&gt;note prevention steps&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It's meant to be simple enough to use during a real incident, but structured enough to make the debugging session actually useful afterward.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Link:&lt;/strong&gt;&lt;br&gt;
&lt;a href="https://trivexia.gumroad.com/l/ci-debugging-template" rel="noopener noreferrer"&gt;CI Debugging Template&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  3) CI Failure Recovery Pack
&lt;/h2&gt;

&lt;p&gt;This bundle combines both products.&lt;/p&gt;

&lt;p&gt;The idea is straightforward:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;use the checklist to find the issue faster&lt;/li&gt;
&lt;li&gt;use the template to document the fix and reduce repeat failures&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you're regularly dealing with broken workflows, the bundle is probably the best buy.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Link:&lt;/strong&gt;&lt;br&gt;
&lt;a href="https://trivexia.gumroad.com/l/ci-failure-recovery-pack" rel="noopener noreferrer"&gt;CI Failure Recovery Pack&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Pricing
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;GitHub Actions Triage Checklist — &lt;strong&gt;$7&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;CI Debugging Template — &lt;strong&gt;$9&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;CI Failure Recovery Pack — &lt;strong&gt;$12&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Who they're for
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;developers using GitHub Actions&lt;/li&gt;
&lt;li&gt;DevOps and platform engineers&lt;/li&gt;
&lt;li&gt;freelancers and consultants managing repos&lt;/li&gt;
&lt;li&gt;small teams without a formal CI troubleshooting process&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;They're not meant to be flashy. They're meant to be useful.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I made them
&lt;/h2&gt;

&lt;p&gt;A lot of CI debugging pain isn't some deep technical mystery.&lt;/p&gt;

&lt;p&gt;It's often just:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;noisy logs&lt;/li&gt;
&lt;li&gt;no structured triage process&lt;/li&gt;
&lt;li&gt;poor documentation after the fact&lt;/li&gt;
&lt;li&gt;repeated failures no one captured properly&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I wanted a simple way to make that workflow cleaner.&lt;/p&gt;

&lt;p&gt;If you've built or bought similar developer products, I'd love feedback on the positioning, packaging, or what would make these more useful in practice.&lt;/p&gt;




&lt;p&gt;If broken CI workflows are eating your time, these should help:&lt;br&gt;
&lt;a href="https://trivexia.gumroad.com/l/ci-failure-recovery-pack" rel="noopener noreferrer"&gt;CI Failure Recovery Pack&lt;/a&gt;&lt;/p&gt;

</description>
      <category>github</category>
      <category>devops</category>
      <category>webdev</category>
      <category>productivity</category>
    </item>
    <item>
      <title>GitHub Actions Security Checklist: 12 Things to Audit Before You Ship</title>
      <dc:creator>Olivier Buitelaar</dc:creator>
      <pubDate>Sat, 21 Mar 2026 03:23:04 +0000</pubDate>
      <link>https://dev.to/ollieb89/github-actions-security-checklist-12-things-to-audit-before-you-ship-7hg</link>
      <guid>https://dev.to/ollieb89/github-actions-security-checklist-12-things-to-audit-before-you-ship-7hg</guid>
      <description>&lt;h1&gt;
  
  
  GitHub Actions Security Checklist: 12 Things to Audit Before You Ship
&lt;/h1&gt;

&lt;p&gt;GitHub Actions is powerful — and that power cuts both ways. A misconfigured workflow can leak secrets, allow unauthorized code execution, or let attackers pivot into your production environment.&lt;/p&gt;

&lt;p&gt;Here's the checklist I run through before shipping any workflow.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. Pin third-party actions to a full commit SHA
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# ❌ Dangerous — tag can be moved&lt;/span&gt;
&lt;span class="pi"&gt;-&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="c1"&gt;# ✅ Safe — immutable&lt;/span&gt;
&lt;span class="pi"&gt;-&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@11bd71901bbe5b1630ceea73d27597364c9af683&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Tags are mutable. An attacker who compromises an upstream action repo can push a new commit to the &lt;code&gt;v4&lt;/code&gt; tag. Pinning to a SHA means you get exactly what you audited.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tool that catches this:&lt;/strong&gt; &lt;a href="https://github.com/marketplace/actions/workflow-guardian" rel="noopener noreferrer"&gt;workflow-guardian&lt;/a&gt; flags unpinned actions automatically.&lt;/p&gt;




&lt;h2&gt;
  
  
  2. Never use &lt;code&gt;pull_request_target&lt;/code&gt; without extreme caution
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;pull_request_target&lt;/code&gt; runs with write permissions and access to secrets — even for PRs from forks. Combined with &lt;code&gt;actions/checkout&lt;/code&gt; on the PR head, you have a critical vulnerability:&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="c1"&gt;# ❌ Pwn request pattern — DO NOT DO THIS&lt;/span&gt;
&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pull_request_target&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;build&lt;/span&gt;&lt;span class="pi"&gt;:&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;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="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;ref&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ github.event.pull_request.head.sha }}&lt;/span&gt;
      &lt;span class="pi"&gt;-&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;npm install &amp;amp;&amp;amp; npm test&lt;/span&gt;  &lt;span class="c1"&gt;# attacker controls this code&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you need &lt;code&gt;pull_request_target&lt;/code&gt;, never check out or execute code from the PR branch.&lt;/p&gt;




&lt;h2&gt;
  
  
  3. Limit &lt;code&gt;GITHUB_TOKEN&lt;/code&gt; permissions
&lt;/h2&gt;

&lt;p&gt;Default token permissions vary by org settings. Be explicit:&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;permissions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;contents&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;read&lt;/span&gt;        &lt;span class="c1"&gt;# only what you need&lt;/span&gt;
  &lt;span class="na"&gt;pull-requests&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;write&lt;/span&gt;  &lt;span class="c1"&gt;# if you post PR comments&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or lock everything down at the workflow level and grant up per-job:&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;permissions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;read-all&lt;/span&gt;  &lt;span class="c1"&gt;# workflow default&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;deploy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;permissions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;contents&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;write&lt;/span&gt;  &lt;span class="c1"&gt;# only this job gets write&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  4. Don't echo untrusted input into $GITHUB_ENV or $GITHUB_OUTPUT
&lt;/h2&gt;

&lt;p&gt;This is an environment variable injection vector:&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="c1"&gt;# ❌ Dangerous if PR title contains shell metacharacters&lt;/span&gt;
&lt;span class="pi"&gt;-&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 "BRANCH=${{ github.event.pull_request.title }}" &amp;gt;&amp;gt; $GITHUB_ENV&lt;/span&gt;

&lt;span class="c1"&gt;# ✅ Use an intermediate env var&lt;/span&gt;
&lt;span class="pi"&gt;-&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 "BRANCH=$TITLE" &amp;gt;&amp;gt; $GITHUB_ENV&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;TITLE&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ github.event.pull_request.title }}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  5. Validate workflow syntax before merge
&lt;/h2&gt;

&lt;p&gt;Broken workflows fail silently or at the worst moment. Lint them:&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="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;Lint workflows&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;ollieb89/workflow-guardian@v1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This catches syntax errors, deprecated features, and security anti-patterns before they hit main.&lt;/p&gt;




&lt;h2&gt;
  
  
  6. Don't store secrets in workflow files
&lt;/h2&gt;

&lt;p&gt;Obvious, but it happens:&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="c1"&gt;# ❌ Never do this&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;API_KEY&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;sk-prod-abc123xyz&lt;/span&gt;

&lt;span class="c1"&gt;# ✅ Use GitHub Secrets&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;API_KEY&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.API_KEY }}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Audit your repo history too — &lt;code&gt;git log -S 'sk-' --all&lt;/code&gt; can surface old leaks.&lt;/p&gt;




&lt;h2&gt;
  
  
  7. Restrict who can trigger &lt;code&gt;workflow_dispatch&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Anyone with write access can trigger manual workflows by default. If your dispatch workflow deploys to production, add an environment with required reviewers.&lt;/p&gt;




&lt;h2&gt;
  
  
  8. Use environments for production deployments
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;deploy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;production&lt;/span&gt;  &lt;span class="c1"&gt;# requires approval from configured reviewers&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Environments give you deployment protection rules, required reviewers, and scoped secrets.&lt;/p&gt;




&lt;h2&gt;
  
  
  9. Check for script injection in &lt;code&gt;run&lt;/code&gt; steps
&lt;/h2&gt;

&lt;p&gt;Any expression interpolated directly into a &lt;code&gt;run&lt;/code&gt; block is a script injection risk:&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="c1"&gt;# ❌ Vulnerable to script injection&lt;/span&gt;
&lt;span class="pi"&gt;-&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 "PR author is ${{ github.event.pull_request.user.login }}"&lt;/span&gt;

&lt;span class="c1"&gt;# ✅ Use env vars&lt;/span&gt;
&lt;span class="pi"&gt;-&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 "PR author is $AUTHOR"&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;AUTHOR&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ github.event.pull_request.user.login }}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  10. Don't use self-hosted runners for public repos
&lt;/h2&gt;

&lt;p&gt;Anyone can fork a public repo and submit a PR that runs on your self-hosted runner. Unless you have strict protections in place, use GitHub-hosted runners for public repos.&lt;/p&gt;




&lt;h2&gt;
  
  
  11. Rotate secrets regularly and audit access
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Check Settings &amp;gt; Secrets for stale/unused entries&lt;/li&gt;
&lt;li&gt;Rotate any secret that may have been exposed in logs&lt;/li&gt;
&lt;li&gt;Grep your workflow run logs for accidental secret prints: &lt;code&gt;***&lt;/code&gt; masking doesn't catch every format&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  12. Enable Dependabot for action version updates
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# .github/dependabot.yml&lt;/span&gt;
&lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;2&lt;/span&gt;
&lt;span class="na"&gt;updates&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;package-ecosystem&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;github-actions&lt;/span&gt;
    &lt;span class="na"&gt;directory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/&lt;/span&gt;
    &lt;span class="na"&gt;schedule&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;interval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;weekly&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This keeps your pinned actions updated with security patches automatically.&lt;/p&gt;




&lt;h2&gt;
  
  
  Automate as Much as Possible
&lt;/h2&gt;

&lt;p&gt;This checklist is a lot to remember. Most of it can be caught automatically:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/marketplace/actions/workflow-guardian" rel="noopener noreferrer"&gt;workflow-guardian&lt;/a&gt;&lt;/strong&gt; — lints your workflow files on every PR, catching security issues and syntax errors before they merge&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/rhysd/actionlint" rel="noopener noreferrer"&gt;actionlint&lt;/a&gt;&lt;/strong&gt; — static analysis for workflow syntax&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dependabot&lt;/strong&gt; — keeps action versions current&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The goal isn't to memorize all of this — it's to build the guardrails so you don't have to.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Found this useful? I build open-source tools for GitHub Actions security. Check out &lt;a href="https://github.com/marketplace/actions/workflow-guardian" rel="noopener noreferrer"&gt;workflow-guardian&lt;/a&gt; — it automates most of this checklist.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>github</category>
      <category>security</category>
      <category>devops</category>
      <category>cicd</category>
    </item>
    <item>
      <title>workflow-guardian vs actionlint: A Technical Deep Dive</title>
      <dc:creator>Olivier Buitelaar</dc:creator>
      <pubDate>Sat, 21 Mar 2026 00:12:10 +0000</pubDate>
      <link>https://dev.to/ollieb89/workflow-guardian-vs-actionlint-a-technical-deep-dive-5b21</link>
      <guid>https://dev.to/ollieb89/workflow-guardian-vs-actionlint-a-technical-deep-dive-5b21</guid>
      <description>&lt;p&gt;&lt;strong&gt;Related articles:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://dev.to/ollieb89/getting-started-with-workflow-guardian-in-5-minutes-45ko"&gt;Getting Started with workflow-guardian in 5 Minutes&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/ollieb89/5-real-github-actions-bugs-caught-by-static-analysis-530a"&gt;5 Real GitHub Actions Bugs Caught by Static Analysis&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/ollieb89/workflow-guardian-vs-actionlint-vs-super-linter-which-github-actions-linter-should-you-use-98j"&gt;workflow-guardian vs actionlint vs super-linter&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Tools mentioned:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/rhysd/actionlint" rel="noopener noreferrer"&gt;actionlint&lt;/a&gt; — Syntax and semantics validation&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/marketplace/actions/workflow-guardian" rel="noopener noreferrer"&gt;workflow-guardian&lt;/a&gt; — Security and best practices&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/ollieb89/test-results-reporter" rel="noopener noreferrer"&gt;test-results-reporter&lt;/a&gt; — Test aggregation&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>githubactions</category>
      <category>devops</category>
      <category>cicd</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Getting Started with workflow-guardian in 5 Minutes</title>
      <dc:creator>Olivier Buitelaar</dc:creator>
      <pubDate>Sat, 21 Mar 2026 00:05:34 +0000</pubDate>
      <link>https://dev.to/ollieb89/getting-started-with-workflow-guardian-in-5-minutes-45ko</link>
      <guid>https://dev.to/ollieb89/getting-started-with-workflow-guardian-in-5-minutes-45ko</guid>
      <description>&lt;p&gt;&lt;strong&gt;Related tools:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/marketplace/actions/workflow-guardian" rel="noopener noreferrer"&gt;workflow-guardian&lt;/a&gt; — CI/CD safety checks&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/ollieb89/test-results-reporter" rel="noopener noreferrer"&gt;test-results-reporter&lt;/a&gt; — Aggregate test results in PR comments&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/ollieb89/pr-size-labeler" rel="noopener noreferrer"&gt;pr-size-labeler&lt;/a&gt; — Auto-label PRs by size&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/ollieb89/stale-branch-cleaner" rel="noopener noreferrer"&gt;stale-branch-cleaner&lt;/a&gt; — Clean up old branches&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/ollieb89/changelog-generator" rel="noopener noreferrer"&gt;changelog-generator&lt;/a&gt; — Auto-generate changelogs&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Questions? Drop them in the comments or open an issue on the repo.&lt;/p&gt;

</description>
      <category>githubactions</category>
      <category>devops</category>
      <category>cicd</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Stop Hardcoding Secrets: 3 Better Ways to Handle GitHub Actions Auth</title>
      <dc:creator>Olivier Buitelaar</dc:creator>
      <pubDate>Fri, 20 Mar 2026 20:43:23 +0000</pubDate>
      <link>https://dev.to/ollieb89/stop-hardcoding-secrets-3-better-ways-to-handle-github-actions-auth-5fpn</link>
      <guid>https://dev.to/ollieb89/stop-hardcoding-secrets-3-better-ways-to-handle-github-actions-auth-5fpn</guid>
      <description>&lt;h1&gt;
  
  
  Stop Hardcoding Secrets: 3 Better Ways to Handle GitHub Actions Auth
&lt;/h1&gt;

&lt;p&gt;You've seen it. Maybe you've even done it. A workflow YAML file with an API key pasted directly into a &lt;code&gt;run:&lt;/code&gt; step. Or a password passed as a command-line argument that shows up in plain text in the CI logs.&lt;/p&gt;

&lt;p&gt;Hardcoding secrets is a disaster waiting to happen. Even if your repo is private today, it might be public tomorrow. Even if it stays private, every developer with read access can now see your production credentials.&lt;/p&gt;

&lt;p&gt;Here are the three patterns I use to handle authentication securely in GitHub Actions.&lt;/p&gt;

</description>
      <category>githubactions</category>
      <category>security</category>
      <category>devops</category>
      <category>opensource</category>
    </item>
    <item>
      <title>5 Real GitHub Actions Bugs Caught by Static Analysis</title>
      <dc:creator>Olivier Buitelaar</dc:creator>
      <pubDate>Fri, 20 Mar 2026 19:18:06 +0000</pubDate>
      <link>https://dev.to/ollieb89/5-real-github-actions-bugs-caught-by-static-analysis-530a</link>
      <guid>https://dev.to/ollieb89/5-real-github-actions-bugs-caught-by-static-analysis-530a</guid>
      <description>&lt;p&gt;&lt;em&gt;You don't find out your CI is broken until it's too late. Here are five real GitHub Actions bugs — and how static analysis catches them before they ever run.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;Static analysis for GitHub Actions workflows is still an underused idea. Most teams lint their application code, type-check their TypeScript, and run SAST on their Python. But the YAML files that orchestrate all of it? Those get copy-pasted from Stack Overflow and committed unchecked.&lt;/p&gt;

&lt;p&gt;These are five categories of real bugs I've seen repeatedly — and how a workflow linter catches them before they cost you anything.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. Secrets Accidentally Echoed in &lt;code&gt;run:&lt;/code&gt; Steps
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The bug:&lt;/strong&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="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;Deploy&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 "Deploying with token: ${{ secrets.DEPLOY_TOKEN }}"&lt;/span&gt;
    &lt;span class="s"&gt;./deploy.sh --token ${{ secrets.DEPLOY_TOKEN }}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That &lt;code&gt;echo&lt;/code&gt; line will print your secret in plain text in the CI logs. GitHub masks &lt;em&gt;known&lt;/em&gt; secret values in logs, but only if the secret is registered correctly — and only in most contexts. If the value gets split across lines or embedded in a longer string, masking can fail silently.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What static analysis catches:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A linter flags any &lt;code&gt;${{ secrets.* }}&lt;/code&gt; reference that appears inside a string passed to &lt;code&gt;echo&lt;/code&gt;, &lt;code&gt;printf&lt;/code&gt;, or similar commands. The fix is simple:&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="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;Deploy&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;DEPLOY_TOKEN&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.DEPLOY_TOKEN }}&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;./deploy.sh --token "$DEPLOY_TOKEN"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Setting secrets as environment variables instead of inline expressions keeps them out of the command string entirely.&lt;/p&gt;




&lt;h2&gt;
  
  
  2. Unpinned Third-Party Actions (Supply Chain Risk)
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The bug:&lt;/strong&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="pi"&gt;-&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;some-org/some-action@main&lt;/span&gt;
&lt;span class="pi"&gt;-&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;another-org/setup-tool@v2&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Using a branch name (&lt;code&gt;@main&lt;/code&gt;) or a mutable tag (&lt;code&gt;@v2&lt;/code&gt;) means your workflow silently runs &lt;em&gt;whatever that action points to tomorrow&lt;/em&gt;. A compromised update to a popular action runs with your repository's full permissions — token, secrets, and all.&lt;/p&gt;

&lt;p&gt;This is a real supply chain risk. There have been several high-profile incidents where popular GitHub Actions were compromised via tag hijacking.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What static analysis catches:&lt;/strong&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="c1"&gt;# Flagged: mutable reference&lt;/span&gt;
&lt;span class="pi"&gt;-&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="c1"&gt;# Clean: pinned to commit SHA&lt;/span&gt;
&lt;span class="pi"&gt;-&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@11bd71901bbe5b1630ceea73d27597364c9af683&lt;/span&gt; &lt;span class="c1"&gt;# v4.2.2&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pinning to a full commit SHA guarantees you're running exactly the code you reviewed. A linter enforces this across your entire workflow directory automatically.&lt;/p&gt;




&lt;h2&gt;
  
  
  3. Missing &lt;code&gt;timeout-minutes&lt;/code&gt; (Runaway Jobs)
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The bug:&lt;/strong&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;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&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;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;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npm install &amp;amp;&amp;amp; npm run build&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No &lt;code&gt;timeout-minutes&lt;/code&gt;. GitHub's default timeout is &lt;strong&gt;6 hours&lt;/strong&gt;. A hung process — npm stuck on a network call, a test waiting for a port that never opens, a deploy step waiting for interactive confirmation — will silently consume your runner for 6 hours.&lt;/p&gt;

&lt;p&gt;On GitHub-hosted runners this costs real money. On self-hosted runners it can block your entire team's CI queue.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What static analysis catches:&lt;/strong&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;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&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;timeout-minutes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;15&lt;/span&gt;  &lt;span class="c1"&gt;# required&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;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;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npm install &amp;amp;&amp;amp; npm run build&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A linter can require &lt;code&gt;timeout-minutes&lt;/code&gt; on every job. This single rule has saved teams significant CI bill spikes.&lt;/p&gt;




&lt;h2&gt;
  
  
  4. &lt;code&gt;continue-on-error: true&lt;/code&gt; Silencing Real Failures
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The bug:&lt;/strong&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="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;Run security scan&lt;/span&gt;
  &lt;span class="na"&gt;continue-on-error&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;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./security-scanner.sh&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;continue-on-error: true&lt;/code&gt; means a failing step is marked as "warning" but doesn't fail the job. This is occasionally legitimate — but when applied to security scans, test suites, or linting steps, failures are silently swallowed and PRs merge anyway.&lt;/p&gt;

&lt;p&gt;Worse, it tends to spread: once one engineer adds it to unblock a stuck PR, others copy the pattern until your quality gates have no teeth.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What static analysis catches:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A linter flags &lt;code&gt;continue-on-error: true&lt;/code&gt; on steps whose names suggest quality gates (scan, test, lint, check) — or warns on any usage and asks for a justification comment. It catches the copy-paste propagation before it becomes policy.&lt;/p&gt;




&lt;h2&gt;
  
  
  5. Deprecated and EOL Runtime Configurations
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The bug:&lt;/strong&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;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;test&lt;/span&gt;&lt;span class="pi"&gt;:&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;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-node@v3&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;node-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;16'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Node 16 has been EOL since GitHub deprecated it in their runner images. Jobs may fail with confusing errors, silently fall back to a different version, or work today but break next month when the runner image drops support.&lt;/p&gt;

&lt;p&gt;Similar issues occur with &lt;code&gt;ubuntu-18.04&lt;/code&gt; runner labels (deprecated), old &lt;code&gt;actions/cache&lt;/code&gt; versions with changed APIs, and patterns like &lt;code&gt;set-output&lt;/code&gt; (deprecated workflow command).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What static analysis catches:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A linter maintains a list of deprecated values and flags them with suggested replacements: &lt;code&gt;node-version: '16'&lt;/code&gt; suggests &lt;code&gt;'20'&lt;/code&gt; or &lt;code&gt;'22'&lt;/code&gt;; &lt;code&gt;ubuntu-18.04&lt;/code&gt; suggests &lt;code&gt;ubuntu-latest&lt;/code&gt;. These are deterministic checks that never need to run a single line of your code.&lt;/p&gt;




&lt;h2&gt;
  
  
  Catching All of This Automatically
&lt;/h2&gt;

&lt;p&gt;Running these checks manually doesn't scale, especially across a monorepo with dozens of workflow files. The solution is making them automatic and zero-friction.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/marketplace/actions/workflow-guardian" rel="noopener noreferrer"&gt;workflow-guardian&lt;/a&gt; is a free GitHub Action that runs all of these checks statically on every PR. Add it in 30 seconds:&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;Validate Workflows&lt;/span&gt;
&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;pull_request&lt;/span&gt;&lt;span class="pi"&gt;]&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;validate&lt;/span&gt;&lt;span class="pi"&gt;:&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;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;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ollieb89/workflow-guardian@v1&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;fail-on-warnings&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It flags all five categories above as annotations directly on the changed workflow files in the PR diff — no separate dashboard, no configuration required to get started.&lt;/p&gt;




&lt;p&gt;Static analysis for your application code is table stakes. Your CI workflows deserve the same treatment. The bugs are there — they just don't show up until something goes wrong at 2am on a Friday.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;What's the worst CI workflow bug you've been bitten by? Drop it in the comments.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>githubactions</category>
      <category>devops</category>
      <category>cicd</category>
      <category>security</category>
    </item>
    <item>
      <title>How to Secure Your GitHub Actions in 5 Minutes: A Step-by-Step Guide</title>
      <dc:creator>Olivier Buitelaar</dc:creator>
      <pubDate>Fri, 20 Mar 2026 13:01:25 +0000</pubDate>
      <link>https://dev.to/ollieb89/how-to-secure-your-github-actions-in-5-minutes-a-step-by-step-guide-9il</link>
      <guid>https://dev.to/ollieb89/how-to-secure-your-github-actions-in-5-minutes-a-step-by-step-guide-9il</guid>
      <description>&lt;h1&gt;
  
  
  How to Secure Your GitHub Actions in 5 Minutes: A Step-by-Step Guide
&lt;/h1&gt;

&lt;p&gt;You've got 100 workflows running across your org. Someone's bound to use &lt;code&gt;pull_request_target&lt;/code&gt; without restrictions. Someone else hardcoded secrets. And nobody's checking permissions.&lt;/p&gt;

&lt;p&gt;This article shows you exactly what to fix — right now, in under 5 minutes.&lt;/p&gt;

&lt;h2&gt;
  
  
  The 5-Minute Security Checklist
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Lock Down Pull Request Workflows (2 minutes)
&lt;/h3&gt;

&lt;p&gt;The biggest GitHub Actions vulnerability is using &lt;code&gt;pull_request_target&lt;/code&gt; with untrusted code.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Bad:&lt;/strong&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;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pull_request_target&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;test&lt;/span&gt;&lt;span class="pi"&gt;:&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;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="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;ref&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ github.event.pull_request.head.sha }}&lt;/span&gt;
      &lt;span class="pi"&gt;-&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;npm test&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This checks out fork code and runs it with your secrets. Disaster.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Good:&lt;/strong&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;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;pull_request&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;types&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;opened&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;synchronize&lt;/span&gt;&lt;span class="pi"&gt;]&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;test&lt;/span&gt;&lt;span class="pi"&gt;:&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;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;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npm test&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Regular &lt;code&gt;pull_request&lt;/code&gt; checks out your repo code, not the fork. Safe.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;If you MUST use &lt;code&gt;pull_request_target&lt;/code&gt;:&lt;/strong&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;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pull_request_target&lt;/span&gt;

&lt;span class="na"&gt;permissions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;contents&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;read&lt;/span&gt;
  &lt;span class="na"&gt;pull-requests&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;read&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;test&lt;/span&gt;&lt;span class="pi"&gt;:&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;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="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;ref&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;refs/pull/${{ github.event.number }}/merge&lt;/span&gt;
      &lt;span class="pi"&gt;-&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;npm test&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Always set explicit minimal permissions. Never checkout the head SHA.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Review Your Secret Handling (2 minutes)
&lt;/h3&gt;

&lt;p&gt;Secrets leak through logs, error messages, and third-party action outputs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Bad:&lt;/strong&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="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;Deploy&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;npm run deploy&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;DATABASE_PASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.DB_PASSWORD }}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This logs the password in step output if deployment fails.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Good:&lt;/strong&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="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;Deploy&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;npm run deploy&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;DATABASE_PASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.DB_PASSWORD }}&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.ref == 'refs/heads/main'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But better: Use GitHub's native secret masking or encrypted deploy environments.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best:&lt;/strong&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;deploy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;environment&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;production&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;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;Deploy&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;npm run deploy&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;DATABASE_PASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.DB_PASSWORD }}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Environments enforce branch protection and approvals.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Pin Action Versions (1 minute)
&lt;/h3&gt;

&lt;p&gt;Never use &lt;code&gt;@latest&lt;/code&gt; or &lt;code&gt;@main&lt;/code&gt;. Pinning to exact commits prevents supply chain attacks.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Bad:&lt;/strong&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="pi"&gt;-&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@latest&lt;/span&gt;
&lt;span class="pi"&gt;-&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;some-org/some-action@main&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Good:&lt;/strong&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="pi"&gt;-&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@a5ac7e51b41094c92402da3b24376905380afc29&lt;/span&gt; &lt;span class="c1"&gt;# v4.1.6&lt;/span&gt;
&lt;span class="pi"&gt;-&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;some-org/some-action@2c9de6ab0de0eb09362a4c5e32f39541eca7d5fa&lt;/span&gt; &lt;span class="c1"&gt;# v2.0.1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Use full commit SHA, add version tag in comment for readability.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tools to help:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/ollieb89/workflow-guardian" rel="noopener noreferrer"&gt;workflow-guardian&lt;/a&gt; — scans your workflows for unpinned actions, hardcoded secrets, unsafe permissions&lt;/li&gt;
&lt;li&gt;actionlint (available as GitHub Action) — lints YAML syntax&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The 1-Minute Audit
&lt;/h2&gt;

&lt;p&gt;Run this locally to find issues:&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;# Install workflow-guardian&lt;/span&gt;
npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; @ollieb89/workflow-guardian

&lt;span class="c"&gt;# Scan your repo&lt;/span&gt;
workflow-guardian scan &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This checks for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Unpinned actions&lt;/li&gt;
&lt;li&gt;Hardcoded secrets&lt;/li&gt;
&lt;li&gt;Overpermissive permissions&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;pull_request_target&lt;/code&gt; without restrictions&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What's Next?
&lt;/h2&gt;

&lt;p&gt;For &lt;strong&gt;continuous enforcement&lt;/strong&gt;, add workflow-guardian to your CI:&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;Lint Workflows&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;pull_request&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;paths&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;.github/workflows/**'&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;lint&lt;/span&gt;&lt;span class="pi"&gt;:&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;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29&lt;/span&gt;
      &lt;span class="pi"&gt;-&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;ollieb89/workflow-guardian@d4e8c9f2a1b3c5e7f9a2b4d6e8f0a1b3c5d7e9f1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your team will never merge an unsafe workflow again.&lt;/p&gt;

&lt;h2&gt;
  
  
  Real Impact
&lt;/h2&gt;

&lt;p&gt;One org I know had 47 workflows with unpinned actions. After pinning and setting permissions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Zero accidental secret leaks (previously 3 per month)&lt;/li&gt;
&lt;li&gt;Supply chain attack surface: 47x → 0&lt;/li&gt;
&lt;li&gt;Enforcement: Manual reviews → Automatic checks&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This 5-minute fix has prevented more actual incidents than I can count.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Need more?&lt;/strong&gt; Check out the full &lt;a href="https://github.com/ollieb89/github-actions-toolkit" rel="noopener noreferrer"&gt;GitHub Actions Security Toolkit&lt;/a&gt; — workflow-guardian, secret scanner, and more.&lt;/p&gt;

</description>
      <category>githubactions</category>
      <category>security</category>
      <category>devops</category>
    </item>
    <item>
      <title>10 GitHub Actions Mistakes That Will Burn You (And How to Avoid Them)</title>
      <dc:creator>Olivier Buitelaar</dc:creator>
      <pubDate>Fri, 20 Mar 2026 07:04:07 +0000</pubDate>
      <link>https://dev.to/ollieb89/10-github-actions-mistakes-that-will-burn-you-and-how-to-avoid-them-3nfd</link>
      <guid>https://dev.to/ollieb89/10-github-actions-mistakes-that-will-burn-you-and-how-to-avoid-them-3nfd</guid>
      <description>&lt;h1&gt;
  
  
  10 GitHub Actions Mistakes That Will Burn You (And How to Avoid Them)
&lt;/h1&gt;

&lt;p&gt;I've spent the last year auditing and fixing GitHub Actions across 17+ repositories. Some mistakes are embarrassing. Some are dangerous. Most are avoidable.&lt;/p&gt;

&lt;p&gt;Here are the ten I see over and over—and exactly how to fix them.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Logging Secrets (The Classic)
&lt;/h2&gt;

&lt;p&gt;You're debugging a failed workflow. The obvious thing to do is &lt;code&gt;echo&lt;/code&gt; your environment variables. So you do:&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="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;Debug&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 "Token is&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.GITHUB_TOKEN }}"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The logs are public. Everyone sees it. Congratulations, your secret is now on the internet.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Never echo secrets in plain text&lt;/li&gt;
&lt;li&gt;Use &lt;code&gt;::add-mask::&lt;/code&gt; to mask values:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&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;Debug&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 "::add-mask::$(echo 'sensitive-value')"&lt;/span&gt;
    &lt;span class="s"&gt;echo "Processing: $(echo 'sensitive-value')"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Workflow Guardian catches this&lt;/strong&gt; by analyzing your workflow YAML for secret exposure patterns. Set it up once, sleep better.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Using Always-Green Credentials (No Secret Rotation)
&lt;/h2&gt;

&lt;p&gt;You created a Personal Access Token (PAT) in 2023 for a GitHub Actions workflow. It's still there. It's never been rotated. It has full repo access. If compromised, the attacker has the same permissions you do.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Use fine-grained PATs with minimal scopes&lt;/li&gt;
&lt;li&gt;Set expiration dates (90-180 days)&lt;/li&gt;
&lt;li&gt;Rotate before expiry&lt;/li&gt;
&lt;li&gt;For CI/CD, prefer GitHub's built-in &lt;code&gt;${{ secrets.GITHUB_TOKEN }}&lt;/code&gt; (auto-scoped, auto-rotated)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  3. Running Untrusted Code from pull_request Events
&lt;/h2&gt;

&lt;p&gt;A user forks your repo, adds a malicious GitHub Action to their fork, opens a PR. Your workflow runs that action. They exfiltrate your secrets.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix:&lt;/strong&gt; Use &lt;code&gt;pull_request_target&lt;/code&gt; for workflows that need access to secrets, but check out the base branch. Or require approval before untrusted code runs.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. No Timeout on Long-Running Jobs
&lt;/h2&gt;

&lt;p&gt;A workflow gets stuck. No one notices. It runs for 6 hours. You burn free tier minutes for nothing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix:&lt;/strong&gt; Set &lt;code&gt;timeout-minutes&lt;/code&gt; globally and per-job to kill stuck workflows automatically.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Hardcoding Environment Values (Config in Code)
&lt;/h2&gt;

&lt;p&gt;You put API endpoints, database hosts, and feature flags directly in your workflow YAML. Now config is versioned, visible in diffs, and mixed with logic.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix:&lt;/strong&gt; Use secrets for anything environment-specific, or repository variables for non-sensitive config.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. No Artifact Cleanup (Storage Bloat)
&lt;/h2&gt;

&lt;p&gt;You upload test results, build artifacts, and logs every run. You've never deleted them. Your storage bill is climbing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix:&lt;/strong&gt; Set explicit retention-days on artifact uploads to clean up automatically after a week or month.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Ignoring Exit Codes (Always Succeeding)
&lt;/h2&gt;

&lt;p&gt;A step fails (exit code 1), but your workflow keeps running and marks the job as successful with &lt;code&gt;|| true&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix:&lt;/strong&gt; Remove &lt;code&gt;|| true&lt;/code&gt; unless you really mean it. Use explicit error handling with &lt;code&gt;continue-on-error: true&lt;/code&gt; when needed.&lt;/p&gt;

&lt;h2&gt;
  
  
  8. No Concurrency Control (Race Conditions in Deployments)
&lt;/h2&gt;

&lt;p&gt;Two PRs merge simultaneously. Both trigger deployment workflows. Both write to production. Chaos.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix:&lt;/strong&gt; Use &lt;code&gt;concurrency&lt;/code&gt; to serialize sensitive jobs. For PR workflows, cancel previous runs on new push.&lt;/p&gt;

&lt;h2&gt;
  
  
  9. Large Monorepo, No Change Detection
&lt;/h2&gt;

&lt;p&gt;You have 20 microservices in one repo. Every push runs tests for all services, even if you only changed one. Slow, expensive, unnecessary.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix:&lt;/strong&gt; Use path filters to only run workflows for changed services. Or use tools like Nx or Turborepo to detect affected packages.&lt;/p&gt;

&lt;h2&gt;
  
  
  10. No Notifications on Failure (Silent Failures)
&lt;/h2&gt;

&lt;p&gt;Your workflow fails. No one knows. Days pass. You merge something that depends on a broken workflow.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix:&lt;/strong&gt; Notify on failure with Slack, email, or GitHub issues. Or set branch protection rules to require passing checks and enable notifications.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Pattern
&lt;/h2&gt;

&lt;p&gt;All ten of these mistakes stem from the same root cause: treating workflows like one-off scripts instead of production infrastructure.&lt;/p&gt;

&lt;p&gt;Workflows run on every commit. They have access to your secrets. They can deploy to production. They touch customer data. Treat them accordingly.&lt;/p&gt;

&lt;p&gt;Three things to do right now:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Audit your workflows&lt;/strong&gt; — look for secrets in logs, overpermissioned tokens, and race conditions&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use Workflow Guardian&lt;/strong&gt; — it catches most of these patterns automatically&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Version your workflow logic&lt;/strong&gt; — don't inline deployment scripts; reference tested, reviewed code&lt;/li&gt;
&lt;/ol&gt;




&lt;p&gt;&lt;strong&gt;Toolkit Links:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/ollieb89/workflow-guardian" rel="noopener noreferrer"&gt;Workflow Guardian&lt;/a&gt; — Detects security issues in your GitHub Actions YAML before production&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/ollieb89/github-actions-toolkit" rel="noopener noreferrer"&gt;GitHub Actions Security Toolkit&lt;/a&gt; — Everything to secure your workflows&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/ollieb89/pr-size-checker" rel="noopener noreferrer"&gt;PR Size Checker&lt;/a&gt; — Catch big PRs before they ship&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/ollieb89/test-result-aggregator" rel="noopener noreferrer"&gt;Test Result Aggregator&lt;/a&gt; — All test results in one place&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Have you hit any of these? What's your biggest workflow mistake? Hit reply.&lt;/p&gt;

</description>
      <category>githubactions</category>
      <category>security</category>
      <category>devops</category>
      <category>productivity</category>
    </item>
  </channel>
</rss>
