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.
The Problem
The workflow usually looks something like this:
- Build a project.
- Push it to GitHub.
- Maybe deploy it somewhere.
- Eventually remember that the project should also appear on your profile.
- Open the profile README.
- Manually add/update the project.
- 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
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
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
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
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
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
---
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
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
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 -->
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 │
└───────────────────────────────┘
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
If they're identical:
No changes detected
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
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
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
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:
- discover my repositories
- read project metadata from them
- read my profile README
- 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
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
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)