DEV Community

Cover image for RepoDeck: Automating the One README I Keep Forgetting to Update
Panic Engineer
Panic Engineer

Posted on

RepoDeck: Automating the One README I Keep Forgetting to Update

I build a lot of projects.

Some are web applications, some are developer tools, some are experiments, and lately I've also been exploring things like browser games and YouTube Playables.

There is one small problem that keeps appearing after every project:

My GitHub Profile README becomes outdated.

The project exists. The repository exists. The code is there.

But my profile still has an old list of projects.

Updating it manually isn't difficult. That's actually what makes the problem interesting.

It's just repetitive enough that I eventually forget to do it.

So I built RepoDeck.

RepoDeck on GitHub

RepoDeck Workflow Diagram


The Problem

The workflow usually looks something like this:

  1. Build a project.
  2. Push it to GitHub.
  3. Maybe deploy it somewhere.
  4. Eventually remember that the project should also appear on your profile.
  5. Open the profile README.
  6. Manually add/update the project.
  7. Repeat.

The actual coding isn't the problem.

The problem is keeping two representations of the same project synchronized:

Project Repository
       │
       │  manually remember
       ▼
GitHub Profile README
Enter fullscreen mode Exit fullscreen mode

And the more projects you have, the more annoying this becomes.

What About Existing README Tools?

There are already tools and generators that can help create GitHub profile READMEs.

But while looking at the problem from my own perspective, I wasn't looking for another README generator.

I didn't want to maintain a separate portfolio dataset.

I didn't want to manually enter every project into another configuration file.

And I didn't want the profile README to become the source of truth for my projects.

The repositories already contain the information.

So I decided to reverse the relationship:

The project repository should be the source of truth. The profile README should be the generated view.

That became the core idea behind RepoDeck.


The RepoDeck Workflow

Instead of manually editing the profile README, each participating repository contains a small metadata file:

.github/project.md
Enter fullscreen mode Exit fullscreen mode

RepoDeck periodically discovers repositories containing that file, validates their metadata, generates the project showcase, and synchronizes it into the profile README.

The resulting workflow is:

Project Repository
       │
       ▼
.github/project.md
       │
       ▼
RepoDeck
       │
       ├── Discover
       ├── Parse
       ├── Validate
       ├── Render
       └── Inject
       │
       ▼
GitHub Profile README
Enter fullscreen mode Exit fullscreen mode

The important part is that the profile README is no longer manually maintained for the generated section.


The Architecture

I deliberately kept RepoDeck small and separated the responsibilities instead of putting everything into one script.

The current structure looks like:

src/
├── index.ts
├── config.ts
├── github/
│   ├── githubClient.ts
│   ├── githubContent.ts
│   └── githubDiscovery.ts
├── markdown/
│   ├── injector.ts
│   ├── parser.ts
│   ├── renderer.ts
│   └── schema.ts
├── services/
│   ├── profileReadme.ts
│   ├── projectDiscovery.ts
│   ├── projectLoader.ts
│   └── readmeUpdater.ts
└── utils/
    ├── fileReader.ts
    └── remoteLoader.ts
Enter fullscreen mode Exit fullscreen mode

This isn't because having more folders automatically makes code better.

It is because the responsibilities are genuinely different.


1. Discovery

RepoDeck first asks GitHub for the authenticated user's repositories.

It doesn't have a hardcoded list of projects.

Instead, repositories opt into RepoDeck by containing:

.github/project.md
Enter fullscreen mode Exit fullscreen mode

That means adding a new project doesn't require modifying RepoDeck itself.

Just add the metadata file to the repository.


2. Metadata as the Contract

The metadata file uses YAML frontmatter.

For example:

---
title: RepoDeck
description: An automated CI/CD pipeline for keeping a GitHub Profile README synchronized with project repositories.
category: Automation
status: Active
featured: true
tech:
  - TypeScript
  - Node.js
  - Octokit
  - Zod
  - GitHub Actions
repo: https://github.com/kaushik0010/repodeck
---
Enter fullscreen mode Exit fullscreen mode

The important design decision here is that the schema only contains information useful for the profile presentation.

For example:

  • title
  • description
  • category
  • status
  • featured
  • tech
  • repository URL
  • live URL

I intentionally didn't try to put everything about a project into this file.

It's portfolio metadata, not another README.


3. Runtime Validation

The metadata comes from repositories outside the core application.

That means I don't trust it blindly.

RepoDeck uses Zod to validate the parsed metadata against a strict schema.

Conceptually:

GitHub
  │
  ▼
project.md
  │
  ▼
YAML Frontmatter
  │
  ▼
Zod Validation
  │
  ├── Valid → Continue
  │
  └── Invalid → Fail
Enter fullscreen mode Exit fullscreen mode

This was one of the things I wanted to learn properly while building the project:

TypeScript types alone don't protect you from malformed external data.

The TypeScript type describes what my application expects.

Zod validates what actually arrived.


4. Rendering

Once the metadata has been validated, RepoDeck turns it into Markdown.

I initially used a Markdown table.

Then I changed my mind.

Tables looked structured, but they became increasingly awkward once I considered:

  • mobile GitHub clients
  • descriptions of different lengths
  • projects with or without links
  • games without repositories
  • projects with only a live URL
  • future categories

So I switched to a showcase-style layout:

### ⭐ RepoDeck

An automated GitHub Profile README synchronization tool.

- 🏷️ Category: Automation
- 🛠 Tech: TypeScript · Node.js · Octokit
- 🔗 GitHub Repository
Enter fullscreen mode Exit fullscreen mode

This is also why I separated the renderer from the rest of the pipeline.

The data acquisition layer doesn't care how the project is displayed.

If I ever wanted a different presentation format, I could replace the renderer without rewriting GitHub discovery.


5. Injection With Explicit Boundaries

RepoDeck doesn't rewrite the entire profile README.

That would be dangerous.

Instead, I added explicit boundaries:

<!-- REPODECK:START -->

<!-- REPODECK:END -->
Enter fullscreen mode Exit fullscreen mode

RepoDeck only owns the content between these markers.

Everything outside them belongs to the user.

The injector also validates that:

  • both markers exist
  • neither marker is duplicated
  • the markers are in the correct order

This gives RepoDeck a clearly defined write boundary.

Profile README

┌───────────────────────────────┐
│ Personal introduction         │
│ Skills                        │
│ Other sections                │
│                               │
│ <!-- REPODECK:START -->       │
│                               │
│   RepoDeck generated content  │
│                               │
│ <!-- REPODECK:END -->         │
│                               │
│ Contact information           │
└───────────────────────────────┘
Enter fullscreen mode Exit fullscreen mode

That separation was important to me.

Automation should know exactly what it owns.


6. Idempotency

This is probably one of my favorite parts of the project.

RepoDeck doesn't create a GitHub commit every time it runs.

Before updating the README, it compares:

current README
        vs
generated README
Enter fullscreen mode Exit fullscreen mode

If they're identical:

No changes detected
Enter fullscreen mode Exit fullscreen mode

and the process stops.

If they're different, only then does RepoDeck update the file.

This gives us:

Same input → same output → no unnecessary commit.

That matters because the workflow runs automatically every day.

Without change detection, an automation that runs daily could create a lot of useless Git history.


7. GitHub SHA and Optimistic Concurrency

While implementing the update step, I also learned about something I hadn't previously thought much about: GitHub's file SHA.

When RepoDeck fetches the profile README, GitHub gives us the current file SHA.

When updating the file, RepoDeck sends that SHA back.

Conceptually:

RepoDeck reads README
       │
       ▼
README + SHA
       │
       │
       ├── Nobody changed it
       │        ↓
       │     Update
       │
       └── Someone changed it
                ↓
             Conflict
Enter fullscreen mode Exit fullscreen mode

This prevents RepoDeck from blindly overwriting a newer version of the README.

It's a small detail, but it introduced me to the idea of optimistic concurrency control in a practical situation.


8. Configuration

I also didn't want application-wide policies scattered throughout the codebase.

So RepoDeck has a deliberately tiny configuration module:

PROJECT_METADATA_PATH
INCLUDE_PRIVATE
INCLUDE_ARCHIVED
COMMIT_MESSAGE
Enter fullscreen mode Exit fullscreen mode

That's it.

I intentionally avoided turning config.ts into a dumping ground for every constant in the application.

For example, the README injection markers remain inside the injector because they're part of the injector's responsibility.

This was another useful architectural lesson:

Not every constant belongs in a global configuration file.


9. GitHub Actions

Once the local pipeline worked, I wanted RepoDeck to actually solve the original problem.

So I moved the execution into GitHub Actions.

The workflow runs daily and can also be triggered manually.

The production pipeline is roughly:

GitHub Actions
      │
      ├── Checkout
      ├── Setup Node 22
      ├── npm ci
      ├── Typecheck
      ├── Build
      └── Run RepoDeck
                │
                ▼
          GitHub API
                │
                ▼
       Profile README
Enter fullscreen mode Exit fullscreen mode

I also added:

  • concurrency protection
  • a five-minute job timeout
  • deterministic npm ci
  • TypeScript type checking
  • a production build
  • least-privilege permissions for the workflow runner

Authentication Was Another Learning Experience

One of the questions I initially had was:

Why not just use GitHub's built-in GITHUB_TOKEN?

Because RepoDeck isn't only modifying the repository where the workflow runs.

It needs to:

  1. discover my repositories
  2. read project metadata from them
  3. read my profile README
  4. update my profile README

The built-in Actions token wasn't the right identity for this architecture.

So for my current personal setup, RepoDeck uses a fine-grained Personal Access Token stored as a GitHub Actions repository secret.

The important distinction is:

GitHub Secret
     │
     ▼
REPODECK_PAT
     │
     ▼
Workflow environment
     │
     ▼
GITHUB_TOKEN
     │
     ▼
Octokit
Enter fullscreen mode Exit fullscreen mode

The actual token never lives in the repository source code.

And because this is an open-source repository, that separation is essential.


What I Learned Building It

The interesting part of RepoDeck wasn't really the final feature.

It was everything I had to understand to make a relatively small automation reliable.

Some of the things I learned while building it:

1. Separate data acquisition from presentation

GitHub discovery shouldn't know how the README looks.

The renderer shouldn't know where projects came from.

That separation made changing the table into a showcase layout almost trivial.

2. External data needs runtime validation

TypeScript can't validate data coming from GitHub.

Zod can.

3. Automation needs idempotency

If a scheduled process produces the same result, it shouldn't create another commit.

4. Write boundaries matter

Automation should never assume it owns an entire document when it only needs one section.

5. Concurrency isn't only a distributed-systems problem

The GitHub SHA mechanism made optimistic concurrency something I could see and implement in a very concrete way.

6. CI catches things local development can hide

One of the first workflow runs failed because TypeScript's strict checks caught an unused API response.

Another failure came from my own project metadata missing a required title.

Those failures were useful.

They proved the CI pipeline was actually doing its job.


Why I Kept RepoDeck Small

There was a temptation to keep adding features.

Multiple renderers.

More metadata fields.

Custom templates.

A plugin system.

A GitHub App.

A marketplace action.

But I stopped.

The current version already solves the problem I originally wanted to solve:

Keep my GitHub Profile README's project section synchronized with my actual repositories without manually maintaining two sources of truth.

I don't want to invent a roadmap just to make the project look bigger.

If I eventually discover a real problem worth solving, I'll build it then.


The Result

Today the workflow is simple:

Build a project
      ↓
Add/update .github/project.md
      ↓
Push
      ↓
RepoDeck discovers it
      ↓
RepoDeck validates the metadata
      ↓
RepoDeck generates the showcase
      ↓
RepoDeck updates the profile README
      ↓
Done
Enter fullscreen mode Exit fullscreen mode

And because the process is scheduled, I don't have to remember to update my profile after every project.

That's exactly what I wanted.

Not a huge platform.

Not a SaaS product.

Just a small piece of automation that removes a repetitive task from my own development workflow.


Open Source

RepoDeck is open source, and I'd genuinely love to see how other developers would approach the same problem.

If you maintain a GitHub profile with multiple projects, I'd especially be interested in hearing how you currently keep your project showcase updated.

Repository: github.com/kaushik0010/repodeck

If you find the idea useful, a ⭐ or fork is always appreciated.

And if you see an architectural decision you'd approach differently, I'd be even more interested in that.


Final Thought

The project started with a very small annoyance:

"I keep forgetting to update my GitHub README."

It ended up teaching me about runtime validation, separation of concerns, idempotency, optimistic concurrency, GitHub APIs, authentication, secrets, CI/CD, scheduled automation, and designing systems around explicit ownership boundaries.

That's probably my favorite kind of project:

one that solves a small real problem while forcing me to learn something substantially bigger.

Top comments (0)