<?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: XenoCyber0</title>
    <description>The latest articles on DEV Community by XenoCyber0 (@xenocyber0).</description>
    <link>https://dev.to/xenocyber0</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4071905%2Fc01d37ce-859c-4c6c-b282-fda80cf1e09d.jpg</url>
      <title>DEV Community: XenoCyber0</title>
      <link>https://dev.to/xenocyber0</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/xenocyber0"/>
    <language>en</language>
    <item>
      <title>I Built a GitHub Action That Writes Your PR Descriptions</title>
      <dc:creator>XenoCyber0</dc:creator>
      <pubDate>Mon, 10 Aug 2026 21:06:48 +0000</pubDate>
      <link>https://dev.to/xenocyber0/i-built-a-github-action-that-writes-your-pr-descriptions-2obp</link>
      <guid>https://dev.to/xenocyber0/i-built-a-github-action-that-writes-your-pr-descriptions-2obp</guid>
      <description>&lt;h1&gt;
  
  
  I Built a GitHub Action That Writes PR Descriptions From Real Diffs (Works With Ollama)
&lt;/h1&gt;

&lt;p&gt;Yep, this does what the title says. StandupBot reads the actual PR diff and commit messages, runs them through any OpenAI-compatible LLM endpoint, and writes a structured Summary / Changes / Testing description — so you never have to write "fixed stuff" again.&lt;/p&gt;

&lt;p&gt;I built this for my own team because I was tired of staring at empty PR boxes after every fix. Then I put it on GitHub Marketplace so anyone can use it. MIT licensed, no strings.&lt;/p&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/XenoCyber0" rel="noopener noreferrer"&gt;
        XenoCyber0
      &lt;/a&gt; / &lt;a href="https://github.com/XenoCyber0/StandUpBot" rel="noopener noreferrer"&gt;
        StandUpBot
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;StandupBot&lt;/h1&gt;
&lt;/div&gt;
&lt;p&gt;&lt;a href="https://github.com/XenoCyber0/StandUpBot/./LICENSE" rel="noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/fdf2982b9f5d7489dcf44570e714e3a15fce6253e0cc6b5aa61a075aac2ff71b/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f4c6963656e73652d4d49542d79656c6c6f772e737667" alt="License: MIT"&gt;&lt;/a&gt;
&lt;a href="https://github.com/marketplace/actions/standupbot" rel="noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/bf6e5e2386a8322562cd681eb199153b0afc0914b20d3906f7dcdc3b0e34c6e0/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f4d61726b6574706c6163652d5374616e647570426f742d707572706c653f6c6f676f3d676974687562" alt="GitHub Marketplace"&gt;&lt;/a&gt;
&lt;a href="https://github.com/XenoCyber0/StandUpBot" rel="noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/1ccf81b69e1f4e483d2a7a74dd96157e812b2830b7e513b3ef673a5ef511b797/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f4f70656e253230536f757263652d2545322539442541342d726564" alt="Open Source Love"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;A GitHub Action that writes your PR descriptions for you. It runs the real diff and commits through an LLM you choose, and fills in a structured &lt;strong&gt;Summary / Changes / Testing&lt;/strong&gt; description plus up to three labels — so you stop hand-writing PR bodies and standup updates.&lt;/p&gt;
&lt;p&gt;Bring your own model: it talks to &lt;strong&gt;any OpenAI-compatible endpoint&lt;/strong&gt; (OpenAI, OpenRouter, Ollama, LM Studio, …). No provider, URL, or model is hardcoded.&lt;/p&gt;

&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Sample output&lt;/h2&gt;
&lt;/div&gt;
&lt;p&gt;Generated end-to-end by StandupBot for a real PR (&lt;a href="https://github.com/nodejs/node/pull/64573" rel="noopener noreferrer"&gt;nodejs/node#64573&lt;/a&gt;) from its actual diff, using a real LLM endpoint. This is the description it produced verbatim:&lt;/p&gt;
&lt;blockquote&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Summary&lt;/h2&gt;
&lt;/div&gt;
&lt;p&gt;Add &lt;code&gt;lchownSync&lt;/code&gt; to the VFS implementation so that symbolic link ownership can be changed without following the link, matching the behavior of &lt;code&gt;fs.lchownSync&lt;/code&gt;.&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Changes&lt;/h2&gt;

&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;doc/api/vfs.md: added &lt;code&gt;lchownSync(path, uid, gid)&lt;/code&gt; to the list of VFS API signatures.&lt;/li&gt;
&lt;li&gt;lib/internal/vfs/file_system.js: added synchronous &lt;code&gt;lchownSync&lt;/code&gt; method to &lt;code&gt;VirtualFileSystem&lt;/code&gt; and updated the async…&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/XenoCyber0/StandUpBot" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;





&lt;h2&gt;
  
  
  The Problem
&lt;/h2&gt;

&lt;p&gt;You open a PR. Forty files changed. Two hours of focused work. The description field is empty.&lt;/p&gt;

&lt;p&gt;So you write: &lt;code&gt;fixed stuff&lt;/code&gt;. Or &lt;code&gt;updated code&lt;/code&gt;. Or &lt;code&gt;changes requested by reviewer&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Three months later, someone (you) is running &lt;code&gt;git log -p&lt;/code&gt; trying to figure out why the config format changed in April. The PR description was supposed to save that investigation. Nobody wrote it.&lt;/p&gt;

&lt;p&gt;We tried before:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Approach&lt;/th&gt;
&lt;th&gt;Why it failed&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;PR templates&lt;/td&gt;
&lt;td&gt;Everyone types "see title" into the template&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Checklist bots&lt;/td&gt;
&lt;td&gt;Nagging doesn't scale; people ignore them&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Reviewer enforcement&lt;/td&gt;
&lt;td&gt;Becomes the team's most hated job&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The gap isn't discipline. It's that writing PR descriptions is &lt;strong&gt;unrewarded work&lt;/strong&gt;. You get the same merge button whether you write a detailed description or "fixed stuff."&lt;/p&gt;

&lt;p&gt;So I built &lt;a href="https://github.com/marketplace/actions/standupbot-pr-describer" rel="noopener noreferrer"&gt;StandupBot&lt;/a&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  What It Produces (Real Example, No Cherry-Picking)
&lt;/h2&gt;

&lt;p&gt;This is verbatim output from &lt;a href="https://github.com/nodejs/node/pull/64573" rel="noopener noreferrer"&gt;nodejs/node#64573&lt;/a&gt;, generated from the actual diff:&lt;/p&gt;

&lt;blockquote&gt;
&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;Add &lt;code&gt;lchownSync&lt;/code&gt; to the VFS implementation so that symbolic link ownership can be changed without following the link, matching the behavior of &lt;code&gt;fs.lchownSync&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;
  
  
  Changes
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;doc/api/vfs.md: added &lt;code&gt;lchownSync(path, uid, gid)&lt;/code&gt; to VFS API signatures.&lt;/li&gt;
&lt;li&gt;lib/internal/vfs/file_system.js: added synchronous &lt;code&gt;lchownSync&lt;/code&gt; method and updated the async wrapper.&lt;/li&gt;
&lt;li&gt;lib/internal/vfs/provider.js: added default &lt;code&gt;lchownSync&lt;/code&gt; method with JSDoc comment.&lt;/li&gt;
&lt;li&gt;lib/internal/vfs/providers/memory.js: implemented &lt;code&gt;lchownSync&lt;/code&gt; that updates uid/gid of the link entry.&lt;/li&gt;
&lt;li&gt;lib/internal/vfs/setup.js: changed handler to invoke &lt;code&gt;vfs.lchownSync&lt;/code&gt; instead of &lt;code&gt;vfs.chownSync&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;test/parallel/test-vfs-lchown-symlink.js: new test verifying sync, callback, and promise variants.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  Testing
&lt;/h2&gt;

&lt;p&gt;The new test exercises &lt;code&gt;fs.lchownSync&lt;/code&gt;, &lt;code&gt;fs.lchown&lt;/code&gt; (callback), and &lt;code&gt;fsp.lchown&lt;/code&gt; (promise) on symlinks inside a VFS mount, asserting correct uid/gid changes.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Nobody edited that. It went straight from the model into the PR body. The action also returns a normalized &lt;code&gt;title&lt;/code&gt; and &lt;code&gt;labels&lt;/code&gt; clamped to &lt;code&gt;bug | feature | chore | docs | refactor&lt;/code&gt; — so your label taxonomy stays clean.&lt;/p&gt;




&lt;h2&gt;
  
  
  30-Second Setup
&lt;/h2&gt;

&lt;p&gt;Add &lt;code&gt;.github/workflows/standupbot.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;StandupBot&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="na"&gt;permissions&lt;/span&gt;&lt;span class="pi"&gt;:&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;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;describe&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;XenoCyber0/StandUpBot@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;llm-base-url&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.LLM_BASE_URL }}&lt;/span&gt;
          &lt;span class="na"&gt;llm-api-key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.LLM_API_KEY }}&lt;/span&gt;
          &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ vars.LLM_MODEL }}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Set &lt;code&gt;LLM_API_KEY&lt;/code&gt; as a &lt;strong&gt;secret&lt;/strong&gt;, &lt;code&gt;LLM_MODEL&lt;/code&gt; as a &lt;strong&gt;variable&lt;/strong&gt;. That's the whole thing.&lt;/p&gt;




&lt;h2&gt;
  
  
  BYO Endpoint: No Vendor Lock-In
&lt;/h2&gt;

&lt;p&gt;The action talks to &lt;strong&gt;any&lt;/strong&gt; OpenAI-compatible chat API. Nothing is hardcoded. All of these work:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;OpenAI        → https://api.openai.com/v1
OpenRouter    → https://openrouter.ai/api/v1
Ollama        → http://localhost:11434/v1
LM Studio     → http://localhost:1234/v1
LocalAI       → http://localai:8080/v1
You decide.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You pick the model. You control the data path. Switching providers later is one secret update.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Self-Hosted Setup (My Favorite Part)
&lt;/h2&gt;

&lt;p&gt;If you want your code to never leave your network:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;GitHub repo ──webhook──▶ self-hosted Actions runner ──HTTP──▶ Ollama
                             (on your LAN)              (your LLM box)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;Run a self-hosted Actions runner on the same LAN as your LLM box&lt;/li&gt;
&lt;li&gt;Point &lt;code&gt;llm-base-url&lt;/code&gt; at your Ollama/LM Studio instance&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;llm-api-key&lt;/code&gt; can be any non-empty string — Ollama ignores it&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Your diff never crosses the internet. The only outbound call is writing the PR body back to GitHub, which... GitHub already owns.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Model notes:&lt;/strong&gt; In my testing, &lt;strong&gt;Qwen2.5-Coder 7B&lt;/strong&gt; and similar 7–9B instruction-tuned models work well. Below ~3B params, file-name hallucinations start appearing.&lt;/p&gt;




&lt;h2&gt;
  
  
  It Never Overwrites You
&lt;/h2&gt;

&lt;p&gt;The rule is simple:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;PR body empty&lt;/strong&gt; → StandupBot writes it&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PR body has StandupBot's marker&lt;/strong&gt; → updates its own output&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PR body has anything else&lt;/strong&gt; → does absolutely nothing&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Your hand-written descriptions are safe. If you ever overwrite what it generated, it takes the hint and stays out.&lt;/p&gt;




&lt;h2&gt;
  
  
  How It Works Internally
&lt;/h2&gt;

&lt;p&gt;Three parts worth stealing:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Diff budget.&lt;/strong&gt; Fetches the PR diff, applies gitignore-style exclusions from &lt;code&gt;.standupbot.yml&lt;/code&gt; before anything hits a prompt:&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;exclude&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;package-lock.json&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;**/*.lock'&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;**/dist/**'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The diff is hard-capped at ~24KB — a monster PR can't blow up your LLM context window.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Map-reduce for big diffs.&lt;/strong&gt; Under the budget, one call. Over it? Each file gets summarized individually (max 8 LLM calls), then merged into the final description.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Structured output enforcement.&lt;/strong&gt; The prompt requires a rigid schema — &lt;code&gt;title&lt;/code&gt;, &lt;code&gt;summary&lt;/code&gt;, &lt;code&gt;changes[]&lt;/code&gt;, &lt;code&gt;testing&lt;/code&gt;, &lt;code&gt;labels[]&lt;/code&gt; — and the parser clamps labels to the allowed set. If the model invents &lt;code&gt;urgent-pls&lt;/code&gt;, it gets dropped, not shipped.&lt;/p&gt;




&lt;h2&gt;
  
  
  Honest Limitations
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Testing section can be inferred, not evidenced.&lt;/strong&gt; Sometimes the model writes "built and renders correctly" for a docs-only change because the pattern is tempting. Treat it as a draft. Known issue, prompt tightening planned.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Huge PRs lose their tail.&lt;/strong&gt; Beyond the character cap, the map-reduce path works on summaries — a detail in file #47 may not survive.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GitHub reads are rate-limited&lt;/strong&gt; (~60/hr) without a token, but the default workflow token covers this.&lt;/li&gt;
&lt;/ul&gt;




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

&lt;ul&gt;
&lt;li&gt;Prompt tightening for the testing-overclaim problem&lt;/li&gt;
&lt;li&gt;More testing on genuinely enormous PRs&lt;/li&gt;
&lt;li&gt;Whatever GitHub issues say — the roadmap is user-adaptive because there are currently about four users&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Try It
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Marketplace:&lt;/strong&gt; &lt;a href="https://github.com/marketplace/actions/standupbot-pr-describer" rel="noopener noreferrer"&gt;github.com/marketplace/actions/standupbot-pr-describer&lt;/a&gt;&lt;br&gt;
&lt;strong&gt;Source:&lt;/strong&gt; &lt;a href="https://github.com/XenoCyber0/StandUpBot" rel="noopener noreferrer"&gt;github.com/XenoCyber0/StandUpBot&lt;/a&gt; (MIT)&lt;br&gt;
&lt;strong&gt;Issues/feedback:&lt;/strong&gt; &lt;a href="https://github.com/XenoCyber0/StandUpBot/issues" rel="noopener noreferrer"&gt;github.com/XenoCyber0/StandUpBot/issues&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Written by the person who finally read "fixed stuff" one too many times and decided to do something about it.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>githubactions</category>
      <category>opensource</category>
      <category>typescript</category>
    </item>
  </channel>
</rss>
