DEV Community

Cover image for Stop Updating Your README Manually: Automate It with GitHub Actions
Sahil Khurana
Sahil Khurana

Posted on Originally published at innostax.com

Stop Updating Your README Manually: Automate It with GitHub Actions

Your README was accurate on the day you wrote it. That was six months and forty commits ago. Someone opened an issue this week saying the install steps don't work. You already know why.

GitHub Actions can keep your README current without you touching it. Build status, contributor counts, latest release tags, weather data, external API responses — any of it can be fetched, formatted through a Go template, and committed back to your repo on a schedule. This guide walks through exactly how to set that up.

Why bother?

Here's the honest version: most project READMEs are unreliable. Not useless — unreliable in the specific way that erodes trust gradually. You follow the install instructions, hit an error, and notice the last README update was fourteen months ago. You sort it out yourself, maybe open a PR, maybe just move on.

That friction compounds. For open source projects especially, a stale README is a soft "do not enter" sign for contributors who don't already know the codebase. They see outdated version numbers or broken setup steps and quietly close the tab.

A dynamic README handles the parts that change predictably — the factual, frequently-shifting data that's easiest to get wrong manually:

  • Current build status — pass or fail, pulled directly from CI
  • Latest release tag — the version shown is always the version that actually shipped
  • Open issue and PR counts — useful shorthand for how active the project is
  • Contributor stats — worth surfacing if you want contributions to feel like they get noticed
  • Dependency freshness — handy for library maintainers or security-conscious teams
  • Auto-generated changelogs — release notes that write themselves instead of getting skipped

It won't fix prose that was poorly written to begin with. But for the factual, frequently-changing parts of your docs, it handles the maintenance burden entirely — and does it better than you would, because it never forgets.

What GitHub Actions is actually doing here

GitHub Actions is a CI/CD platform built directly into GitHub. You define workflows in YAML files, GitHub runs them on their infrastructure, and you get access to your repo, a Linux environment, and a large library of pre-built actions you can chain together.

For this use case, the critical feature is scheduled execution. You can tell a workflow to run every hour, every day at a specific time, every 15 minutes — independent of whether anyone pushed anything. The cron fires, the workflow fetches fresh data, the template renders, and if anything changed from the last run, a commit lands automatically. Nobody pressed anything.

Workflow triggers come in four types: repository events (push, pull request, issue activity), external webhooks, scheduled cron expressions, and manual dispatch from the Actions tab. For dynamic READMEs, cron is almost always what you want.

💡 Worth knowing before you set your cron frequency: the GitHub Actions free tier gives you 2,000 minutes per month for private repos. An hourly workflow that takes about a minute each run costs roughly 720 minutes monthly. For public repos it's unlimited. Factor that in.

Go templates — the rendering layer

Go templates handle the translation from raw data to formatted output. The syntax uses double curly braces — {{ and }} — to mark where dynamic values slot in. Go's standard library text/template package handles the rendering, but you don't need to write any Go code yourself here. The action takes care of execution; your job is just writing the template file.

The mental model is simple enough to hold in your head:

Data + Template = Output

You write a README.md.template file that looks exactly like a normal markdown README, except the dynamic sections are tagged with template syntax. When the workflow runs, it passes your configured data through the template and writes the result to README.md. That rendered file is what gets committed.

The three template functions worth knowing: template renders a named sub-template (useful for tables or repeated structures), Execute renders the full template against a data object, and ExecuteTemplate lets you target a specific named section within a larger file. In practice, the template tags in your .template file are all you'll interact with.

Setting it up — four steps

None of these are complicated, but the order matters.

Step 1 — Create your template file

In your repository root, create README.md.template. This is now the file you edit going forward. README.md becomes a generated output — don't edit it directly, because the next workflow run will overwrite it.

Write your standard README content in the template file. Then add placeholder tags wherever you want live data:

\# Today's hourly forecast:

{{ template "hourly-table" $todayWeather.HourlyWeathers }}

\# Multi-day forecast:

{{ template "daily-table" .Weathers }}

\# Last-updated timestamp:

{{ formatTime.UpdatedAt }}
Enter fullscreen mode Exit fullscreen mode

Everything outside those tags renders as plain markdown. The action only processes what you've explicitly tagged — the rest passes through untouched.

If your template lives in a subfolder, say docs/README.md.template, you'll update the template-file path in the workflow config. Same goes for out-file if your README isn't in the root.

Step 2 — Get a weather API key

The weather data comes from WeatherAPI.com. Register for a free account — it takes about two minutes and the free tier covers far more calls than an hourly cron job needs. Once you have a key, add it to your repository as an encrypted secret: Settings → Secrets and variables → Actions → New repository secret. Name it WEATHER_API_KEY.

Secrets are never visible in workflow logs. The ${{ secrets.WEATHER_API_KEY }} syntax in your YAML reads it at runtime without exposing it anywhere.

Step 3 — Create the workflow file

Create .github/workflows/update-weather.yml with the following content:

name: "Cronjob"

on:

schedule:

\- cron: '15 \* \* \* \*'

jobs:

update-weather:

permissions: write-all

runs-on: ubuntu-latest

steps:

\- uses: actions/checkout@v3

\- name: Generate README

uses: Shubham-Sharma-Innostax/weather@v1.0.0

with:

city: Gurgaon

days: 7

weather-api-key: ${{ secrets.WEATHER\_API\_KEY }}

template-file: 'README.md.template'

out-file: 'README.md'

\- name: Commit

run: |

if git diff --exit-code; then

echo "No changes to commit."

exit 0

else

git config user.name github-actions

git config user.email github-actions@github.com

git add .

git commit -m "update"

git push origin main

fi
Enter fullscreen mode Exit fullscreen mode

Step 4 — Adjust the variables

Three things to change before you push:

  • city — replace Gurgaon with the city you want weather data for
  • days — number of forecast days to include, anywhere from 1 to 7
  • template-file — update the path if your template isn't in the repository root

The cron expression '15 * * * *' runs at 15 minutes past every hour. Hourly is reasonable for weather data. For slower-changing things — release tags, contributor counts — daily is enough and saves you free-tier minutes.

The commit step — why it's written the way it is

The commit block is doing something worth understanding, because it isn't just blindly pushing on every run.

Before it touches git at all, it runs git diff --exit-code. That command exits with code 0 when nothing changed, and code 1 when something did. The if block reads that exit code: if the weather data is identical to the last run, the workflow logs "No changes to commit" and exits cleanly. Your commit history stays clean — no empty commits piling up every hour.

When something actually changed, it configures a bot identity for the commit, stages everything, commits with a plain "update" message, and pushes. The whole thing typically takes under 30 seconds.

⚠️ One thing that catches people: if your default branch is named master rather than main, update the git push line at the bottom. It's an easy miss and the failure message isn't obvious.

What else this pattern handles

Weather is just the proof of concept. The same mechanics apply to almost anything you can pull from an API or generate from your own repository data.

  • GitHub API — star count, fork count, open PR count, all available without authentication for public repos
  • Latest release — pull your most recent tag and render it with a link to the release notes, so the version displayed is always the version that shipped
  • Top contributors — fetch contributor data from the GitHub API and render a credits section that updates itself as people contribute
  • RSS or changelog feed — if you publish release notes anywhere with a feed, the latest entries can be pulled and embedded directly
  • CI badge refresh — force your status badge to regenerate on a schedule, not just on push

For each of these, the pattern stays the same: write a data-fetching step, write a Go template for the display format, wire them together in the workflow YAML. The structure doesn't change much between use cases.

⚠️ A caveat worth stating plainly: the more external data sources you depend on, the more failure modes you introduce. If the weather API goes down during a cron run, your workflow fails. Add basic error handling — even just a fallback message in the template — before you rely on this for anything that matters.

One less thing to think about

Documentation maintenance slips through the cracks not because developers don't care, but because it's invisible work. Nobody files a bug when the README goes stale. It just gradually stops being trustworthy.

The setup here takes an hour — probably less if you've worked with GitHub Actions before. After that, the parts of your README that could drift out of date update themselves. You stop thinking about them.

That's really what you're after. Not a better manual process. No process at all.

Push the workflow file, wait for the first cron run, check the result. If the README shows fresh data, you're done.

Read more - How to Create Dynamic ReadMe File via Github Actions
──────────────────────────────────────────────────────────

Have you set up a dynamic README on your own project? What data sources are you pulling in — release tags, contributor counts, something else entirely? Drop your setup in the comments. 👇

*Sahil Khurana - CTO, Innostax
*

Founded in 2014, Innostax is a software development company built on accountability and ownership. We deliver progress with clarity—flagging risks early, aligning teams, and ensuring quality at every step. With a commitment to responsibility and reliability, we take full ownership of everything we build, making software development seamless and dependable.

Top comments (0)