Someone visiting your GitHub profile wants to get to know you. They want to see who you are, what you build, what you write. But most profile READMEs are written once and then left untouched for years. A "latest posts" list from 2021, expired projects, a tech stack still waiting to be updated.
What if your README updated itself every morning?
In this article we will build a system that automatically pulls your Medium and dev.to articles, runs daily via GitHub Actions, and keeps your README alive. We will walk through every step: the Python script, the workflow file, placeholder markers, and manual triggering.
1. How Does It Work? 🔄
The system we are building has four parts:
We place special HTML comment lines inside README.md (like <!-- MEDIUM-ARTICLES:START -->). The script finds the content between these markers and replaces it; it does not touch anything outside the markers.
The Python script calls Medium's RSS feed and dev.to's REST API, collects article titles and URLs, then updates the README.
The GitHub Actions workflow runs this script on a schedule. It triggers every day at 06:00 UTC; if the script makes any changes, it automatically commits and pushes.
The manual trigger option means you do not have to wait. The moment you publish a new article, you can run it with a single click from the Actions tab.
💡 The entire system runs on GitHub's own infrastructure. No external server, no paid service, and your machine does not need to be on. GitHub Actions is free for public repos.
2. What Is a GitHub Profile README? 👤
When you create a repository with the same name as your username, GitHub automatically displays the README.md inside it on your profile page. This is a special GitHub feature.
How to Create the Profile README Repo
- Click the New repository button on GitHub
- Enter your own username as the repository name (e.g.
yasinatesim) - Make sure Public is selected
- Check the Add a README file checkbox
- Click Create repository
When the repo is created, GitHub greets you with "✨ yasinatesim/yasinatesim is a special repository." Everything you write in README.md starts appearing on your profile.
⚠️ The repository name must be exactly the same as your username, including case.
Yasinatesimandyasinatesimare different repos and the profile feature will not work.
3. Adding Placeholder Markers to the README 📍
How does the script know what to update in the README? Through special markers we place as HTML comment lines. These markers are invisible in the browser and on GitHub, but they act as coordinates for the script.
Add the following block wherever you want articles to appear in your README:
<span class="gu">## ✍️ Latest Medium Articles</span>
<span class="c"><!-- MEDIUM-ARTICLES:START --></span>
<span class="c"><!-- MEDIUM-ARTICLES:END --></span>
<span class="gu">## 📝 Latest dev.to Articles</span>
<span class="c"><!-- DEVTO-ARTICLES:START --></span>
<span class="c"><!-- DEVTO-ARTICLES:END --></span>
When the script runs, it deletes everything between the two markers and writes the current article list in its place. It does not touch any line outside the markers. After the first run the output will look like this:
<span class="gu">## ✍️ Latest Medium Articles</span>
<span class="c"><!-- MEDIUM-ARTICLES:START --></span>
<span class="p">-</span> <span class="p">[</span><span class="nv">Micro Frontend Architecture (with React Examples)</span><span class="p">](</span><span class="sx">https://medium.com/...</span><span class="p">)</span>
<span class="p">-</span> <span class="p">[</span><span class="nv">How We Integrated Google Lighthouse into Our Dev Process</span><span class="p">](</span><span class="sx">https://medium.com/...</span><span class="p">)</span> <span class="ge">*(Hepsiburadatech)*</span>
<span class="p">-</span> <span class="p">[</span><span class="nv">TypeScript from A to Z</span><span class="p">](</span><span class="sx">https://medium.com/...</span><span class="p">)</span>
<span class="c"><!-- MEDIUM-ARTICLES:END --></span>
💡 The example above is from my own GitHub Profile README. Publication info (the channel name in parentheses) is extracted automatically from the article URL by the script. When it sees a URL like medium.com/publication/..., it grabs the slug and adds it in parentheses next to the article title (shown on the second line above). For personal profile articles (medium.com/@yasinatesim/...) it shows no parentheses.
4. Python Script: Fetching Article Data 🐍
The entire script uses two external libraries: feedparser (for the Medium RSS feed) and requests (for the dev.to API). No other dependencies.
scripts/
└── fetch_articles.py
4.1 Basic Structure
<span class="sh">"""</span><span class="s">
fetch_articles.py
-----------------
Fetches all articles from Medium RSS and the dev.to API,
then automatically updates the placeholders in README.md.
</span><span class="sh">"""</span>
<span class="kn">import</span> <span class="n">re</span>
<span class="kn">import</span> <span class="n">feedparser</span>
<span class="kn">import</span> <span class="n">requests</span>
<span class="n">MEDIUM_USERNAME</span> <span class="o">=</span> <span class="sh">"</span><span class="s">your-username</span><span class="sh">"</span> <span class="c1"># without the @ sign
</span><span class="n">DEVTO_USERNAME</span> <span class="o">=</span> <span class="sh">"</span><span class="s">your-username</span><span class="sh">"</span>
<span class="n">README_PATH</span> <span class="o">=</span> <span class="sh">"</span><span class="s">README.md</span><span class="sh">"</span>
<span class="n">MEDIUM_START</span> <span class="o">=</span> <span class="sh">"</span><span class="s"><!-- MEDIUM-ARTICLES:START --></span><span class="sh">"</span>
<span class="n">MEDIUM_END</span> <span class="o">=</span> <span class="sh">"</span><span class="s"><!-- MEDIUM-ARTICLES:END --></span><span class="sh">"</span>
<span class="n">DEVTO_START</span> <span class="o">=</span> <span class="sh">"</span><span class="s"><!-- DEVTO-ARTICLES:START --></span><span class="sh">"</span>
<span class="n">DEVTO_END</span> <span class="o">=</span> <span class="sh">"</span><span class="s"><!-- DEVTO-ARTICLES:END --></span><span class="sh">"</span>
4.2 Fetching Medium Articles
Medium provides a public RSS feed for every user. feedparser converts this XML feed into Python objects. We do not set a limit; we process the entire feed.entries list.
<span class="k">def</span> <span class="nf">fetch_medium_articles</span><span class="p">():</span>
<span class="n">url</span> <span class="o">=</span> <span class="sa">f</span><span class="sh">"</span><span class="s">https://medium.com/feed/@</span><span class="si">{</span><span class="n">MEDIUM_USERNAME</span><span class="si">}</span><span class="sh">"</span>
<span class="n">feed</span> <span class="o">=</span> <span class="n">feedparser</span><span class="p">.</span><span class="nf">parse</span><span class="p">(</span><span class="n">url</span><span class="p">)</span>
<span class="n">articles</span> <span class="o">=</span> <span class="p">[]</span>
<span class="k">for</span> <span class="n">entry</span> <span class="ow">in</span> <span class="n">feed</span><span class="p">.</span><span class="n">entries</span><span class="p">:</span>
<span class="n">publication</span> <span class="o">=</span> <span class="bp">None</span>
<span class="n">link</span> <span class="o">=</span> <span class="n">entry</span><span class="p">.</span><span class="n">link</span>
<span class="c1"># Extract publication info from the URL
</span> <span class="c1"># medium.com/publication-slug/... → publication exists
</span> <span class="c1"># medium.com/@username/... → personal profile, no publication
</span> <span class="n">subdomain_match</span> <span class="o">=</span> <span class="n">re</span><span class="p">.</span><span class="nf">match</span><span class="p">(</span><span class="sa">r</span><span class="sh">"</span><span class="s">https://([^.]+)\.medium\.com/</span><span class="sh">"</span><span class="p">,</span> <span class="n">link</span><span class="p">)</span>
<span class="n">path_match</span> <span class="o">=</span> <span class="n">re</span><span class="p">.</span><span class="nf">match</span><span class="p">(</span><span class="sa">r</span><span class="sh">"</span><span class="s">https://medium\.com/([^@/][^/]*)/</span><span class="sh">"</span><span class="p">,</span> <span class="n">link</span><span class="p">)</span>
<span class="k">if</span> <span class="n">subdomain_match</span><span class="p">:</span>
<span class="n">slug</span> <span class="o">=</span> <span class="n">subdomain_match</span><span class="p">.</span><span class="nf">group</span><span class="p">(</span><span class="mi">1</span><span class="p">)</span>
<span class="n">publication</span> <span class="o">=</span> <span class="n">slug</span><span class="p">.</span><span class="nf">replace</span><span class="p">(</span><span class="sh">"</span><span class="s">-</span><span class="sh">"</span><span class="p">,</span> <span class="sh">"</span><span class="s"> </span><span class="sh">"</span><span class="p">).</span><span class="nf">title</span><span class="p">()</span>
<span class="k">elif</span> <span class="n">path_match</span><span class="p">:</span>
<span class="n">slug</span> <span class="o">=</span> <span class="n">path_match</span><span class="p">.</span><span class="nf">group</span><span class="p">(</span><span class="mi">1</span><span class="p">)</span>
<span class="c1"># Filter out system paths (tag, search, topic, etc.)
</span> <span class="n">system_paths</span> <span class="o">=</span> <span class="p">{</span><span class="sh">"</span><span class="s">tag</span><span class="sh">"</span><span class="p">,</span> <span class="sh">"</span><span class="s">tags</span><span class="sh">"</span><span class="p">,</span> <span class="sh">"</span><span class="s">search</span><span class="sh">"</span><span class="p">,</span> <span class="sh">"</span><span class="s">topic</span><span class="sh">"</span><span class="p">,</span> <span class="sh">"</span><span class="s">topics</span><span class="sh">"</span><span class="p">,</span>
<span class="sh">"</span><span class="s">m</span><span class="sh">"</span><span class="p">,</span> <span class="sh">"</span><span class="s">about</span><span class="sh">"</span><span class="p">,</span> <span class="sh">"</span><span class="s">membership</span><span class="sh">"</span><span class="p">}</span>
<span class="k">if</span> <span class="n">slug</span> <span class="ow">not</span> <span class="ow">in</span> <span class="n">system_paths</span><span class="p">:</span>
<span class="n">publication</span> <span class="o">=</span> <span class="n">slug</span><span class="p">.</span><span class="nf">replace</span><span class="p">(</span><span class="sh">"</span><span class="s">-</span><span class="sh">"</span><span class="p">,</span> <span class="sh">"</span><span class="s"> </span><span class="sh">"</span><span class="p">).</span><span class="nf">title</span><span class="p">()</span>
<span class="n">articles</span><span class="p">.</span><span class="nf">append</span><span class="p">({</span>
<span class="sh">"</span><span class="s">title</span><span class="sh">"</span><span class="p">:</span> <span class="n">entry</span><span class="p">.</span><span class="n">title</span><span class="p">,</span>
<span class="sh">"</span><span class="s">url</span><span class="sh">"</span><span class="p">:</span> <span class="n">link</span><span class="p">,</span>
<span class="sh">"</span><span class="s">publication</span><span class="sh">"</span><span class="p">:</span> <span class="n">publication</span><span class="p">,</span>
<span class="p">})</span>
<span class="k">return</span> <span class="n">articles</span>
⚠️ Medium's RSS feed returns only the last 10 articles by default. This is a limit set by Medium; no matter what your script does, it cannot pull more than 10 via RSS. If you want to show all your articles, you will need to look into scraping approaches.
I do not yet have 10 published articles on Medium, so I have not integrated this into my own GitHub Profile README yet. 😁 I will update the repo once I cross 10 articles. The repo link is in the Demo section at the end.
4.3 Fetching dev.to Articles
dev.to has an open REST API that requires no API key. Since it supports pagination, we can pull all articles with a while loop:
<span class="k">def</span> <span class="nf">fetch_devto_articles</span><span class="p">():</span>
<span class="n">page</span><span class="p">,</span> <span class="n">articles</span> <span class="o">=</span> <span class="mi">1</span><span class="p">,</span> <span class="p">[]</span>
<span class="k">while</span> <span class="bp">True</span><span class="p">:</span>
<span class="n">url</span> <span class="o">=</span> <span class="p">(</span>
<span class="sa">f</span><span class="sh">"</span><span class="s">https://dev.to/api/articles</span><span class="sh">"</span>
<span class="sa">f</span><span class="sh">"</span><span class="s">?username=</span><span class="si">{</span><span class="n">DEVTO_USERNAME</span><span class="si">}</span><span class="s">&per_page=100&page=</span><span class="si">{</span><span class="n">page</span><span class="si">}</span><span class="sh">"</span>
<span class="p">)</span>
<span class="n">response</span> <span class="o">=</span> <span class="n">requests</span><span class="p">.</span><span class="nf">get</span><span class="p">(</span><span class="n">url</span><span class="p">,</span> <span class="n">timeout</span><span class="o">=</span><span class="mi">10</span><span class="p">)</span>
<span class="k">if</span> <span class="n">response</span><span class="p">.</span><span class="n">status_code</span> <span class="o">!=</span> <span class="mi">200</span><span class="p">:</span>
<span class="k">break</span>
<span class="n">batch</span> <span class="o">=</span> <span class="n">response</span><span class="p">.</span><span class="nf">json</span><span class="p">()</span>
<span class="k">if</span> <span class="ow">not</span> <span class="n">batch</span><span class="p">:</span> <span class="c1"># Empty page → no more data
</span> <span class="k">break</span>
<span class="k">for</span> <span class="n">item</span> <span class="ow">in</span> <span class="n">batch</span><span class="p">:</span>
<span class="n">articles</span><span class="p">.</span><span class="nf">append</span><span class="p">({</span>
<span class="sh">"</span><span class="s">title</span><span class="sh">"</span><span class="p">:</span> <span class="n">item</span><span class="p">[</span><span class="sh">"</span><span class="s">title</span><span class="sh">"</span><span class="p">],</span>
<span class="sh">"</span><span class="s">url</span><span class="sh">"</span><span class="p">:</span> <span class="n">item</span><span class="p">[</span><span class="sh">"</span><span class="s">url</span><span class="sh">"</span><span class="p">],</span>
<span class="p">})</span>
<span class="n">page</span> <span class="o">+=</span> <span class="mi">1</span>
<span class="k">return</span> <span class="n">articles</span>
4.4 Building Markdown Rows
<span class="k">def</span> <span class="nf">build_medium_rows</span><span class="p">(</span><span class="n">articles</span><span class="p">):</span>
<span class="k">if</span> <span class="ow">not</span> <span class="n">articles</span><span class="p">:</span>
<span class="k">return</span> <span class="sh">"</span><span class="s">_No articles found._</span><span class="sh">"</span>
<span class="n">rows</span> <span class="o">=</span> <span class="p">[]</span>
<span class="k">for</span> <span class="n">a</span> <span class="ow">in</span> <span class="n">articles</span><span class="p">:</span>
<span class="c1"># Show publication in italic parentheses if present
</span> <span class="n">pub</span> <span class="o">=</span> <span class="sa">f</span><span class="sh">"</span><span class="s"> *(</span><span class="si">{</span><span class="n">a</span><span class="p">[</span><span class="sh">'</span><span class="s">publication</span><span class="sh">'</span><span class="p">]</span><span class="si">}</span><span class="s">)*</span><span class="sh">"</span> <span class="k">if</span> <span class="n">a</span><span class="p">.</span><span class="nf">get</span><span class="p">(</span><span class="sh">"</span><span class="s">publication</span><span class="sh">"</span><span class="p">)</span> <span class="k">else</span> <span class="sh">""</span>
<span class="n">rows</span><span class="p">.</span><span class="nf">append</span><span class="p">(</span><span class="sa">f</span><span class="sh">'</span><span class="s">- [</span><span class="si">{</span><span class="n">a</span><span class="p">[</span><span class="sh">"</span><span class="s">title</span><span class="sh">"</span><span class="p">]</span><span class="si">}</span><span class="s">](</span><span class="si">{</span><span class="n">a</span><span class="p">[</span><span class="sh">"</span><span class="s">url</span><span class="sh">"</span><span class="p">]</span><span class="si">}</span><span class="s">)</span><span class="si">{</span><span class="n">pub</span><span class="si">}</span><span class="sh">'</span><span class="p">)</span>
<span class="k">return</span> <span class="sh">"</span><span class="se">\n</span><span class="sh">"</span><span class="p">.</span><span class="nf">join</span><span class="p">(</span><span class="n">rows</span><span class="p">)</span>
<span class="k">def</span> <span class="nf">build_devto_rows</span><span class="p">(</span><span class="n">articles</span><span class="p">):</span>
<span class="k">if</span> <span class="ow">not</span> <span class="n">articles</span><span class="p">:</span>
<span class="k">return</span> <span class="sh">"</span><span class="s">_No articles found._</span><span class="sh">"</span>
<span class="k">return</span> <span class="sh">"</span><span class="se">\n</span><span class="sh">"</span><span class="p">.</span><span class="nf">join</span><span class="p">(</span>
<span class="sa">f</span><span class="sh">'</span><span class="s">- [</span><span class="si">{</span><span class="n">a</span><span class="p">[</span><span class="sh">"</span><span class="s">title</span><span class="sh">"</span><span class="p">]</span><span class="si">}</span><span class="s">](</span><span class="si">{</span><span class="n">a</span><span class="p">[</span><span class="sh">"</span><span class="s">url</span><span class="sh">"</span><span class="p">]</span><span class="si">}</span><span class="s">)</span><span class="sh">'</span> <span class="k">for</span> <span class="n">a</span> <span class="ow">in</span> <span class="n">articles</span>
<span class="p">)</span>
4.5 Updating the README
We use regex to find the block between the markers and replace it with the new content. The re.DOTALL flag makes . match newlines as well; without it, multi-line blocks are not captured.
<span class="k">def</span> <span class="nf">replace_section</span><span class="p">(</span><span class="n">content</span><span class="p">,</span> <span class="n">start_marker</span><span class="p">,</span> <span class="n">end_marker</span><span class="p">,</span> <span class="n">new_body</span><span class="p">):</span>
<span class="n">pattern</span> <span class="o">=</span> <span class="n">re</span><span class="p">.</span><span class="nf">compile</span><span class="p">(</span>
<span class="sa">rf</span><span class="sh">"</span><span class="si">{</span><span class="n">re</span><span class="p">.</span><span class="nf">escape</span><span class="p">(</span><span class="n">start_marker</span><span class="p">)</span><span class="si">}</span><span class="s">.*?</span><span class="si">{</span><span class="n">re</span><span class="p">.</span><span class="nf">escape</span><span class="p">(</span><span class="n">end_marker</span><span class="p">)</span><span class="si">}</span><span class="sh">"</span><span class="p">,</span>
<span class="n">re</span><span class="p">.</span><span class="n">DOTALL</span><span class="p">,</span>
<span class="p">)</span>
<span class="n">replacement</span> <span class="o">=</span> <span class="sa">f</span><span class="sh">"</span><span class="si">{</span><span class="n">start_marker</span><span class="si">}</span><span class="se">\n</span><span class="si">{</span><span class="n">new_body</span><span class="si">}</span><span class="se">\n</span><span class="si">{</span><span class="n">end_marker</span><span class="si">}</span><span class="sh">"</span>
<span class="k">return</span> <span class="n">pattern</span><span class="p">.</span><span class="nf">sub</span><span class="p">(</span><span class="n">replacement</span><span class="p">,</span> <span class="n">content</span><span class="p">)</span>
<span class="k">def</span> <span class="nf">main</span><span class="p">():</span>
<span class="nf">print</span><span class="p">(</span><span class="sh">"</span><span class="s">📡 Fetching Medium articles...</span><span class="sh">"</span><span class="p">)</span>
<span class="n">medium_articles</span> <span class="o">=</span> <span class="nf">fetch_medium_articles</span><span class="p">()</span>
<span class="nf">print</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="s"> ✅ </span><span class="si">{</span><span class="nf">len</span><span class="p">(</span><span class="n">medium_articles</span><span class="p">)</span><span class="si">}</span><span class="s"> articles fetched.</span><span class="sh">"</span><span class="p">)</span>
<span class="nf">print</span><span class="p">(</span><span class="sh">"</span><span class="s">📡 Fetching dev.to articles...</span><span class="sh">"</span><span class="p">)</span>
<span class="n">devto_articles</span> <span class="o">=</span> <span class="nf">fetch_devto_articles</span><span class="p">()</span>
<span class="nf">print</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="s"> ✅ </span><span class="si">{</span><span class="nf">len</span><span class="p">(</span><span class="n">devto_articles</span><span class="p">)</span><span class="si">}</span><span class="s"> articles fetched.</span><span class="sh">"</span><span class="p">)</span>
<span class="k">with</span> <span class="nf">open</span><span class="p">(</span><span class="n">README_PATH</span><span class="p">,</span> <span class="sh">"</span><span class="s">r</span><span class="sh">"</span><span class="p">,</span> <span class="n">encoding</span><span class="o">=</span><span class="sh">"</span><span class="s">utf-8</span><span class="sh">"</span><span class="p">)</span> <span class="k">as</span> <span class="n">f</span><span class="p">:</span>
<span class="n">content</span> <span class="o">=</span> <span class="n">f</span><span class="p">.</span><span class="nf">read</span><span class="p">()</span>
<span class="n">content</span> <span class="o">=</span> <span class="nf">replace_section</span><span class="p">(</span>
<span class="n">content</span><span class="p">,</span> <span class="n">MEDIUM_START</span><span class="p">,</span> <span class="n">MEDIUM_END</span><span class="p">,</span> <span class="nf">build_medium_rows</span><span class="p">(</span><span class="n">medium_articles</span><span class="p">)</span>
<span class="p">)</span>
<span class="n">content</span> <span class="o">=</span> <span class="nf">replace_section</span><span class="p">(</span>
<span class="n">content</span><span class="p">,</span> <span class="n">DEVTO_START</span><span class="p">,</span> <span class="n">DEVTO_END</span><span class="p">,</span> <span class="nf">build_devto_rows</span><span class="p">(</span><span class="n">devto_articles</span><span class="p">)</span>
<span class="p">)</span>
<span class="k">with</span> <span class="nf">open</span><span class="p">(</span><span class="n">README_PATH</span><span class="p">,</span> <span class="sh">"</span><span class="s">w</span><span class="sh">"</span><span class="p">,</span> <span class="n">encoding</span><span class="o">=</span><span class="sh">"</span><span class="s">utf-8</span><span class="sh">"</span><span class="p">)</span> <span class="k">as</span> <span class="n">f</span><span class="p">:</span>
<span class="n">f</span><span class="p">.</span><span class="nf">write</span><span class="p">(</span><span class="n">content</span><span class="p">)</span>
<span class="nf">print</span><span class="p">(</span><span class="sh">"</span><span class="s">✅ README.md updated successfully!</span><span class="sh">"</span><span class="p">)</span>
<span class="k">if</span> <span class="n">__name__</span> <span class="o">==</span> <span class="sh">"</span><span class="s">__main__</span><span class="sh">"</span><span class="p">:</span>
<span class="nf">main</span><span class="p">()</span>
5. GitHub Actions Workflow File ⚙️
The workflow file goes inside the .github/workflows/ folder. GitHub automatically scans this folder and treats YAML files as workflows.
.github/
└── workflows/
└── update-articles.yml
File contents:
<span class="na">name</span><span class="pi">:</span> <span class="s">📝 Update Blog Articles</span>
<span class="na">on</span><span class="pi">:</span>
<span class="na">schedule</span><span class="pi">:</span>
<span class="pi">-</span> <span class="na">cron</span><span class="pi">:</span> <span class="s2">"</span><span class="s">0</span><span class="nv"> </span><span class="s">6</span><span class="nv"> </span><span class="s">*</span><span class="nv"> </span><span class="s">*</span><span class="nv"> </span><span class="s">*"</span> <span class="c1"># Every day at 06:00 UTC</span>
<span class="na">workflow_dispatch</span><span class="pi">:</span> <span class="c1"># For manual triggering from the Actions tab</span>
<span class="na">jobs</span><span class="pi">:</span>
<span class="na">update-readme</span><span class="pi">:</span>
<span class="na">name</span><span class="pi">:</span> <span class="s">Fetch & Update Articles</span>
<span class="na">runs-on</span><span class="pi">:</span> <span class="s">ubuntu-latest</span>
<span class="na">steps</span><span class="pi">:</span>
<span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">🔄 Checkout Repository</span>
<span class="na">uses</span><span class="pi">:</span> <span class="s">actions/checkout@v4</span>
<span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">🐍 Set up Python</span>
<span class="na">uses</span><span class="pi">:</span> <span class="s">actions/setup-python@v5</span>
<span class="na">with</span><span class="pi">:</span>
<span class="na">python-version</span><span class="pi">:</span> <span class="s2">"</span><span class="s">3.11"</span>
<span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">📦 Install Dependencies</span>
<span class="na">run</span><span class="pi">:</span> <span class="s">pip install requests feedparser</span>
<span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">🚀 Run Article Fetcher</span>
<span class="na">run</span><span class="pi">:</span> <span class="s">python scripts/fetch_articles.py</span>
<span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">💾 Commit & Push Changes</span>
<span class="na">run</span><span class="pi">:</span> <span class="pi">|</span>
<span class="s">git config --local user.email "github-actions[bot]@users.noreply.github.com"</span>
<span class="s">git config --local user.name "github-actions[bot]"</span>
<span class="s">git add README.md</span>
<span class="s">git diff --staged --quiet || git commit -m "📝 Auto-update: Latest blog articles [$(date +'%Y-%m-%d')]"</span>
<span class="s">git push</span>
actions/checkout@v4 -- Copies your repo to the virtual machine (ubuntu-latest) where the workflow runs. Without it the script cannot find README.md.
actions/setup-python@v5 -- Sets up the Python 3.11 environment. Even though GitHub Actions machines come with Python pre-installed, pinning the version is safer.
pip install requests feedparser -- Installs the two dependencies.
python scripts/fetch_articles.py -- Runs the script. This step updates README.md but does not commit yet.
Commit & Push step -- The critical line is:
git diff <span class="nt">--staged</span> <span class="nt">--quiet</span> <span class="o">||</span> git commit <span class="nt">-m</span> <span class="s2">"..."</span>
git diff --staged --quiet returns a non-zero exit code when there are changes in the README; thanks to the || operator, a commit is made only if there are changes, and nothing happens otherwise. Without this, an empty commit would be created on every run.
💡 Cron syntax: 0 6 * * * means minute=0, hour=6, every day, every month, every day of the week. Keep in mind it uses UTC. For a different time, use crontab.guru.
6. Manual Triggering 🖱️
The cron job runs every morning, but you might not want to wait after publishing a new article. The workflow_dispatch trigger lets you run it whenever you want:
- Go to your repository on GitHub
- Click the Actions tab at the top
- Click 📝 Update Blog Articles in the left menu
- Click the Run workflow button that appears on the right
- Click Run workflow again in the dropdown that opens
The workflow starts within a few seconds. A yellow spinning icon appears in the left menu; once it finishes it turns green ✅. When you look at your README, you will see the updated article list.
7. Demo 👀
You can see a working example of the system described in this article on my own GitHub profile. Medium and dev.to articles update automatically every morning.
The full source code, including fetch_articles.py and update-articles.yml, is available in the repo below:
👉 github.com/yasinatesim/yasinatesim
Feedback 📬
While writing this article, I used Claude Opus 4.6 Thinking for proofreading and research, and Gemini 3.1 Flash Image Preview (Nano Banana 2) for generating the diagrams.
Feedback, suggestions, and corrections are always welcome. You can reach me through the social media links on my website or on LinkedIn.
Best, Yasin 🤗
Resources 📚
Docs
- GitHub Actions -- Workflow syntax
- GitHub Actions -- Events (schedule, workflow_dispatch)
- dev.to API Docs
- feedparser Docs
Tools
- crontab.guru -- Visually test cron expressions
- GitHub Secrets Docs -- Store API keys securely







