<?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: Mahendra S</title>
    <description>The latest articles on DEV Community by Mahendra S (@mahendra_xp).</description>
    <link>https://dev.to/mahendra_xp</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%2F3910120%2F25ff06fa-6a78-4cc0-8939-2ac6fb7aa1e0.png</url>
      <title>DEV Community: Mahendra S</title>
      <link>https://dev.to/mahendra_xp</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/mahendra_xp"/>
    <language>en</language>
    <item>
      <title>How I built a tool that checks if a GitHub issue already has a PR — and why that's the feature nobody built</title>
      <dc:creator>Mahendra S</dc:creator>
      <pubDate>Tue, 05 May 2026 03:30:00 +0000</pubDate>
      <link>https://dev.to/mahendra_xp/how-i-built-a-tool-that-checks-if-a-github-issue-already-has-a-pr-and-why-thats-the-feature-nc7</link>
      <guid>https://dev.to/mahendra_xp/how-i-built-a-tool-that-checks-if-a-github-issue-already-has-a-pr-and-why-thats-the-feature-nc7</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Faywwjv4x709maky7chwv.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Faywwjv4x709maky7chwv.jpg" alt="GitTrek - Find open source issues and track GitHub badge progress" width="800" height="336"&gt;&lt;/a&gt;&lt;br&gt;
I kept doing this.&lt;/p&gt;

&lt;p&gt;Find a beginner-friendly issue. Fork the repo. Set up the dev environment. Read through the codebase. Start working.&lt;/p&gt;

&lt;p&gt;Then check the issue again and see a comment from 2 days ago: &lt;em&gt;"Hey I'm working on this, should have a PR up soon."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Two hours wasted. Every single time.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The weird part? Almost every existing tool for finding open source issues - &lt;em&gt;goodfirstissue.dev&lt;/em&gt;, &lt;em&gt;up-for-grabs.net&lt;/em&gt;, &lt;em&gt;codetriage&lt;/em&gt; - rely on static lists or periodic scrapes. They show you issues, but they don't show you the &lt;strong&gt;live status&lt;/strong&gt; of whether someone is already quietly working on it right now.&lt;/p&gt;

&lt;p&gt;So I built &lt;strong&gt;GitTrek&lt;/strong&gt; to fix this.&lt;/p&gt;
&lt;h2&gt;
  
  
  The core problem: Silent Claiming
&lt;/h2&gt;

&lt;p&gt;When a developer starts working on an issue, they often don't comment or ask for assignment. They just start. But they usually leave one of these "digital traces":&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;PR Mentions&lt;/strong&gt;: They mention the issue number in a PR description or commit message (&lt;code&gt;Fixes #847&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Linked Branches&lt;/strong&gt;: They click "Create a branch for this issue" in GitHub's UI, then open a PR from that branch.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Both leave detectable events in GitHub's GraphQL API that most discovery tools never bother to check.&lt;/p&gt;
&lt;h2&gt;
  
  
  The two GraphQL events that make it work
&lt;/h2&gt;

&lt;p&gt;GitHub's &lt;code&gt;timelineItems&lt;/code&gt; field on issues exposes every event in an issue's history. Two specific types are the key:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;CROSS_REFERENCED_EVENT&lt;/code&gt;&lt;/strong&gt; - Fires when someone mentions the issue number (&lt;code&gt;#847&lt;/code&gt;) in a PR or commit. The PR is the &lt;strong&gt;&lt;code&gt;source&lt;/code&gt;&lt;/strong&gt; of this event.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;CONNECTED_EVENT&lt;/code&gt;&lt;/strong&gt; - Fires when a branch linked to the issue becomes a pull request. In this event, the PR is the &lt;strong&gt;&lt;code&gt;subject&lt;/code&gt;&lt;/strong&gt;, not the source. &lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Pro Tip:&lt;/strong&gt; This is a common GraphQL "gotcha." &lt;code&gt;source&lt;/code&gt; for one, &lt;code&gt;subject&lt;/code&gt; for the other. Mix them up, and you get null results with zero errors.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;
  
  
  The Query
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight graphql"&gt;&lt;code&gt;&lt;span class="err"&gt;issue(number:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;$issueNumber)&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="n"&gt;timelineItems&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="n"&gt;first&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;25&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="n"&gt;itemTypes&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="n"&gt;CROSS_REFERENCED_EVENT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;CONNECTED_EVENT&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="n"&gt;nodes&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="k"&gt;on&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;CrossReferencedEvent&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="n"&gt;source&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="k"&gt;on&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;PullRequest&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="n"&gt;number&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="c"&gt;# OPEN | CLOSED | MERGED&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="n"&gt;isDraft&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="k"&gt;on&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ConnectedEvent&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="n"&gt;subject&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="k"&gt;on&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;PullRequest&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="n"&gt;number&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="n"&gt;isDraft&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;span class="n"&gt;linkedBranches&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;first&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3&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="n"&gt;totalCount&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;h2&gt;
  
  
  The Classification Logic
&lt;/h2&gt;

&lt;p&gt;GitTrek uses this data to color-code your search results instantly:&lt;br&gt;
| Status | Condition |&lt;br&gt;
|---|---|&lt;br&gt;
| 🔴 &lt;strong&gt;Active PR&lt;/strong&gt; | Open non-draft PR exists — high competition |&lt;br&gt;
| 🟡 &lt;strong&gt;Someone started&lt;/strong&gt; | Draft PR linked — proceed carefully |&lt;br&gt;
| 🟡 &lt;strong&gt;Branch exists&lt;/strong&gt; | &lt;code&gt;linkedBranches &amp;gt; 0&lt;/code&gt;, no PR yet — early signal |&lt;br&gt;
| ✅ &lt;strong&gt;Safe to claim&lt;/strong&gt; | No linked PRs or branches found |&lt;/p&gt;


&lt;h2&gt;
  
  
  High Performance: Running checks in parallel
&lt;/h2&gt;

&lt;p&gt;Checking 20 issues means 21 total API calls (1 search + 20 status checks). To keep the UI snappy, GitTrek doesn't block the initial results. We display the issues immediately and then "hydrate" the competition status badges in parallel:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Fetch fresh data from APIs in the background&lt;/span&gt;
&lt;span class="c1"&gt;// This ensures one badge failure doesn't block the entire dashboard&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;settlements&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;allSettled&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
  &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`/api/github/badges/pull-shark?username=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()),&lt;/span&gt;
  &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`/api/github/badges/starstruck?username=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()),&lt;/span&gt;
  &lt;span class="c1"&gt;// ... more badge checks&lt;/span&gt;
&lt;span class="p"&gt;]);&lt;/span&gt;

&lt;span class="c1"&gt;// Safely extract results even if some failed&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;pullShark&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;starstruck&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;settlements&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; 
  &lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;fulfilled&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Apply fallback values for failed items&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;psData&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;pullShark&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;count&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I used &lt;code&gt;Promise.allSettled&lt;/code&gt; instead of &lt;code&gt;Promise.all&lt;/code&gt; for a reason: if one check fails (e.g., due to a repository permission issue or a single-item rate limit), the rest of your dashboard stays functional.&lt;/p&gt;

&lt;h2&gt;
  
  
  What else GitTrek does
&lt;/h2&gt;

&lt;p&gt;Beyond "Ghost PR" detection, I built GitTrek to be a full companion for open-source growth:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Repository Quality Gates: Filter by stars, forks, and whether the repo actually has a CONTRIBUTING.md.&lt;/li&gt;
&lt;li&gt;Live Achievement Tracking: Track your progress toward Pull Shark, Galaxy Brain, and YOLO badges using live GraphQL calculations.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The best part? It suggests a "Focus Mission". If you're only 2 PRs away from Gold Pull Shark, &lt;strong&gt;GitTrek&lt;/strong&gt; will build a custom search query to help you find the exact issues needed to close that gap.&lt;/p&gt;

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

&lt;p&gt;GitTrek is free, open source, and requires no setup for browsing. You only need to connect your GitHub account if you want to track your personalized badge progress.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Live App:&lt;/strong&gt; &lt;a href="https://gittrek.vercel.app" rel="noopener noreferrer"&gt;gittrek.vercel.app&lt;/a&gt; &lt;br&gt;
&lt;strong&gt;Source Code:&lt;/strong&gt; &lt;a href="https://github.com/mahendra-shah/GitTrek" rel="noopener noreferrer"&gt;github.com/mahendra-shah/GitTrek&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;One question for you:&lt;/strong&gt; Have you ever wasted time on &lt;br&gt;
an issue that was already being worked on? How did you &lt;br&gt;
handle it? Drop it in the comments - curious how common &lt;br&gt;
this actually is.&lt;/p&gt;

</description>
      <category>opensource</category>
      <category>github</category>
      <category>webdev</category>
      <category>beginners</category>
    </item>
  </channel>
</rss>
