<?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: Aidan Urbina</title>
    <description>The latest articles on DEV Community by Aidan Urbina (@aidan_urbina).</description>
    <link>https://dev.to/aidan_urbina</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%2F3986222%2F6db46bd9-e297-463b-8894-d5e0d14a4ca2.png</url>
      <title>DEV Community: Aidan Urbina</title>
      <link>https://dev.to/aidan_urbina</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/aidan_urbina"/>
    <language>en</language>
    <item>
      <title>Status updates that write themselves from your git activity</title>
      <dc:creator>Aidan Urbina</dc:creator>
      <pubDate>Mon, 15 Jun 2026 22:44:16 +0000</pubDate>
      <link>https://dev.to/aidan_urbina/status-updates-that-write-themselves-from-your-git-activity-13pe</link>
      <guid>https://dev.to/aidan_urbina/status-updates-that-write-themselves-from-your-git-activity-13pe</guid>
      <description>&lt;p&gt;I've typed the same status update hundreds of times. "Worked on the auth refactor, still going on it, no blockers." Then I'd open the PR I'd literally just pushed and stare at the description I'd already written there, and the commits I'd already named, and think: I just typed all of this. Twice. Why.&lt;/p&gt;

&lt;p&gt;We still do async standups, and I think they're useful. But a standup is the wrong place to first learn what your teammates &lt;em&gt;did&lt;/em&gt;. By the time you're reading it, the work is hours old and someone retyped it by hand. The "what I did" part already exists. You generated it when you opened the PR and named the commits. Asking a developer to also type it into a separate box is asking them to be a worse version of &lt;code&gt;git log&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;So when we built &lt;a href="https://github.com/gosparq/sparq" rel="noopener noreferrer"&gt;sparQ&lt;/a&gt;, an open-source developer-experience suite for teams that live on GitHub, we tried to make one part of this disappear. Not the standup. The status-typing. Its first product, &lt;strong&gt;Pulse&lt;/strong&gt;, is a GitHub-native project management tool, and in it each person's recent activity fills itself in from the work they're already doing, so the standups and updates sit on top of that instead of repeating it.&lt;/p&gt;

&lt;p&gt;The mechanism is simple: GitHub webhooks in, status lines out. Everything that turned out to matter was in the details.&lt;/p&gt;

&lt;h2&gt;
  
  
  The core idea
&lt;/h2&gt;

&lt;p&gt;GitHub already knows what you did, and it'll tell you over a webhook the instant it happens. We subscribe to three event types (&lt;code&gt;push&lt;/code&gt;, &lt;code&gt;pull_request&lt;/code&gt;, and &lt;code&gt;issues&lt;/code&gt;), and the job is to turn that firehose into a feed a human actually wants to read. Push and PR events become a person's current status; issue events drive a separate two-way sync I'll come back to at the end.&lt;/p&gt;

&lt;p&gt;That's the whole architecture. The wiring took an afternoon. The judgment calls took the rest of the week, and they're what the rest of this is about.&lt;/p&gt;

&lt;h2&gt;
  
  
  Can you trust it?
&lt;/h2&gt;

&lt;p&gt;A webhook URL is a public endpoint. Anyone can POST JSON at it, so if you take the payload on faith, I can post activity as you. GitHub signs every delivery with an HMAC of the body, so we verify that signature before doing anything else.&lt;/p&gt;

&lt;p&gt;Two non-obvious things bit us here. First, you have to hash the raw request bytes. If you parse the JSON and re-serialize it, your bytes won't match GitHub's and every signature fails. Second, compare the signatures with a constant-time check, not &lt;code&gt;==&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;hmac&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;compare_digest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;expected&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;signature_header&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A plain string compare returns as soon as two characters differ, which leaks enough timing information to guess the signature byte by byte. It never shows up in testing and it's the first thing a security review flags.&lt;/p&gt;

&lt;p&gt;We also let unsigned webhooks through in development and fail closed in production. Local end-to-end testing already means standing up an ngrok tunnel and a real GitHub App; making people also wire up a shared secret before anything works is how you lose them in the first ten minutes.&lt;/p&gt;

&lt;h2&gt;
  
  
  How fast can you let go of it?
&lt;/h2&gt;

&lt;p&gt;GitHub retries on any non-2xx and is impatient about slow responses, so you never want a database write or an outbound API call sitting between you and your &lt;code&gt;200&lt;/code&gt;. The endpoint does the bare minimum: verify the signature, look up which workspace this installation belongs to, hand the payload to a background worker, and return immediately.&lt;/p&gt;

&lt;p&gt;The catch with going async is that the background thread has no request context, so the tenant the event belongs to vanishes unless you carry it across yourself. We capture the workspace id while we still have the request and re-establish it inside the worker. Every multi-tenant background job has some version of this footgun: it runs fine in your single-tenant test and then writes to the wrong customer in production.&lt;/p&gt;

&lt;h2&gt;
  
  
  What do you say about it?
&lt;/h2&gt;

&lt;p&gt;This is the part I expected to be trivial and wasn't. A raw push payload is not something anyone wants in a feed. The real work is deciding what to throw away.&lt;/p&gt;

&lt;p&gt;The output is a single plain line, like &lt;code&gt;Pushed 3 commits to main: handle expired refresh tokens&lt;/code&gt; or &lt;code&gt;Opened PR #214: fix token refresh race&lt;/code&gt;. Most of the work is throwing things away to get there.&lt;/p&gt;

&lt;p&gt;The one that surprised me was merge commits. Merge a PR and GitHub fires the &lt;code&gt;pull_request&lt;/code&gt; "merged" event and, separately, a &lt;code&gt;push&lt;/code&gt; for the merge commit it generates on your behalf. Honor both and every merge lands in the feed twice. So we sniff out that auto-generated commit and skip the push:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;head_msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startswith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Merge pull request &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;PR events have the opposite problem, which is too many of them. &lt;code&gt;synchronize&lt;/code&gt; fires on every push to the branch, and then there's &lt;code&gt;labeled&lt;/code&gt;, &lt;code&gt;edited&lt;/code&gt;, &lt;code&gt;assigned&lt;/code&gt;, and a dozen more that nobody would call a status. Only &lt;code&gt;opened&lt;/code&gt;, &lt;code&gt;reopened&lt;/code&gt;, &lt;code&gt;ready_for_review&lt;/code&gt;, and the final merged-or-closed actually say something, so those are the only actions that produce a line. Branch deletes and tag pushes carry no commits at all, so they say nothing either.&lt;/p&gt;

&lt;p&gt;The rule underneath all of it: when in doubt, post nothing. An empty feed is fine. A noisy one gets muted, and a muted feed is the same as no feed, except you paid to build it. This part is unglamorous and it's most of what makes the feed feel trustworthy instead of spammy.&lt;/p&gt;

&lt;h2&gt;
  
  
  Who does it belong to?
&lt;/h2&gt;

&lt;p&gt;GitHub identifies people by a numeric user id; sparQ has its own users. So each person links their GitHub account once, and after that a webhook's actor id resolves to a sparQ member. If there's no mapping, we drop the event on the floor rather than posting it.&lt;/p&gt;

&lt;p&gt;That last decision is deliberate. A status feed is about people. "What is Sam up to." So an authorless post isn't a degraded post, it's a category error. Dropping unmapped actors also quietly filters out the noise you'd never want anyway, like Dependabot pushing forty commits with nobody's face on them.&lt;/p&gt;

&lt;h2&gt;
  
  
  The harder half: keeping two systems honest
&lt;/h2&gt;

&lt;p&gt;Reading activity is one-way and forgiving. The issue sync is two-way, and two-way sync has a failure mode one-way doesn't: the infinite loop.&lt;/p&gt;

&lt;p&gt;Close an issue on GitHub and we close the linked task in sparQ. Fine. But closing the task fires sparQ's own "task changed" listener, which wants to push that change &lt;em&gt;back&lt;/em&gt; to GitHub, which fires another webhook, which closes the task again. You've built a machine whose only job is to bury GitHub's API in your own echo.&lt;/p&gt;

&lt;p&gt;The fix is boring and it works: a flag that marks "this change originated from a sync, don't bounce it back."&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;_SYNC_IN_PROGRESS&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;
&lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;resolver_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;note&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Closed via GitHub&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;finally&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;_SYNC_IN_PROGRESS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pop&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The outbound listener checks that flag before pushing anything to GitHub and stays quiet when it's set. The &lt;code&gt;resolver_id=None&lt;/code&gt; does double duty as a marker that GitHub made the change rather than a person, which keeps the activity log honest about who did what.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it adds up to
&lt;/h2&gt;

&lt;p&gt;Connect a repo, link your GitHub account once, go write code. Your recent activity fills in by itself, and nobody typed it. It's just true, because it's the same events GitHub already recorded.&lt;/p&gt;

&lt;p&gt;We still write standups. They're better now, because nobody spends them reciting what they already did in git. The "what I did" is already on the board, so the update can be the part a machine can't generate: what you're stuck on, what you're planning, what you want a second opinion on.&lt;/p&gt;

&lt;p&gt;The lesson I keep relearning is that most "the team needs visibility" problems aren't missing data. The data's there. It's just trapped in a system that won't talk to the one people are looking at. We didn't generate anything new. We listened, threw most of it away, and wrote the survivors as sentences.&lt;/p&gt;

&lt;p&gt;If any of this resonated, the repo is at &lt;strong&gt;&lt;a href="https://github.com/gosparq/sparq" rel="noopener noreferrer"&gt;github.com/gosparq/sparq&lt;/a&gt;&lt;/strong&gt; (AGPL v3, self-hostable). Clone it, &lt;code&gt;docker compose up&lt;/code&gt;, and connect a repo to watch your feed fill itself in. The GitHub connector with the parts I glossed over lives in &lt;strong&gt;&lt;a href="https://github.com/gosparq/sparq/tree/master/pulse/modules/integrations/github" rel="noopener noreferrer"&gt;&lt;code&gt;pulse/modules/integrations/github/&lt;/code&gt;&lt;/a&gt;&lt;/strong&gt;. A ⭐ helps other teams find it, and I'd genuinely like to hear how the "post nothing when in doubt" rule holds up against your team's git habits. Issues and PRs welcome.&lt;/p&gt;

</description>
      <category>showdev</category>
      <category>opensource</category>
      <category>python</category>
      <category>github</category>
    </item>
  </channel>
</rss>
