dev.izo.red
Hi, I'm Reda Izo, a CGI Artist & Photographer based in Brussels.
I spend most of my time thinking about light — and lately, about code.

How I Auto-Publish My Release Notes to My Portfolio, Straight from Claude Code
Pro Tips
Basics
I got tired of shipping something and then having to go update my portfolio about it. The whole point of building in public is that people see the work happening, but copy-pasting release notes into a CMS after every feature felt like the most boring possible tax on that idea.
So I automated it. Now when I cut a release from the terminal, my portfolio site at dev.izo.red updates on its own. Here's exactly how it works and how you can set it up yourself.
What the end result looks like
When I finish a session with Claude Code and cut a release, this is the sequence:
Claude bumps the version, commits, tags, and pushes
GitHub Actions fires on
release: publishedA workflow dispatches to my profile repo
My
CHANGELOG.jsongets a new entryMy Framer site fetches that JSON and renders it live
From my side, I type one command. The site updates itself.
The pieces involved
Before getting into the how, it helps to know what's actually doing the work:
Claude Code — the AI coding assistant I run in the terminal. It handles version bumps, changelog entries, and cutting the release.
GitHub Actions — two separate workflows, one in the project repo and one in my profile repo.
repository_dispatch— a GitHub event that lets one repo trigger a workflow in a different repo. This is the key piece most tutorials skip.Personal Access Token — a classic PAT with
reposcope. More on why this specific kind matters in a moment.CHANGELOG.json — a structured JSON file sitting in my GitHub profile repo (
username/username). It's the single source of truth.Framer — my portfolio site, with a custom Code component (
ChangelogFeed.tsx) that fetches the JSON directly from GitHub and renders it on every page load.
The interesting part is that this spans two repos. That's what makes it feel more complex than it is, and what the repository_dispatch step exists to solve.
Step 1 — The release workflow (your project repo)
The first workflow lives in your project repo — whatever you're actually building and releasing. It fires whenever a GitHub release is published, grabs the release metadata, and sends it across to your profile repo as a custom event.
Add .github/workflows/notify-changelog.yml:
The workflow_dispatch trigger at the top is worth keeping — it lets you manually re-fire the workflow if something breaks downstream, without having to create a dummy release.
Step 2 — The receiver workflow (your profile repo)
This one sits in your GitHub profile repo (username/username) and waits for the release-published event from step 1. When it arrives, it reads your CHANGELOG.json, prepends the new entry, and commits it back.
Add .github/workflows/update-changelog.yml:
The ID generation is worth a look: it reads the existing entries, finds the highest numeric ID, and increments it. Simple and it works even if you've manually added entries with non-sequential IDs.
Step 3 — The token (the part that trips people up)
Here's the thing that catches almost everyone: GITHUB_TOKEN is scoped to the repo it's running in. It cannot trigger workflows in a different repo. If you use it for the repository_dispatch call in step 1, the dispatch will silently fail — no error, just nothing happens on the other side.
You need a classic Personal Access Token with repo scope:
GitHub → Settings → Developer settings → Personal access tokens → Tokens (classic)
Generate new token, check
repoCopy the token immediately, you only see it once
Add it as a
CHANGELOG_TOKENsecret in every project repo that will dispatch
⚠️ PAT gotcha — fine-grained tokens won't work here
GitHub's fine-grained Personal Access Tokens look like the modern choice, but they'll silently fail for cross-repo
repository_dispatchcalls unless you've explicitly granted the token access to every target repo with both Contents and Actions read/write permissions — configured per-repo, not globally. Even then, edge cases can return a 401 with no useful error message.Use a classic PAT with
reposcope instead. One token covers all your repos,repository_dispatchworks immediately, and you won't spend an hour debugging a permissions matrix. Add it as a secret namedCHANGELOG_TOKENin every project repo that needs to dispatch.
Step 4 — The CHANGELOG.json schema
The JSON itself is straightforward. One root key, an array of entries, newest first:
type controls the badge colour on the Framer site — launch for a first release, update for everything after.
The description field is where it gets opinionated. Because Framer fetches this as raw JSON and your component parses it, you need a consistent format your renderer understands. The one that works well:
##for section headers (Added / Changed / Fixed)•with 2-space indent for each itemBacktick-wrap the first term on each line:
`Feature name` — what it doesNo markdown
**bold**, no-list syntax, no per-item emojis
It looks like a slightly stripped-down markdown dialect, which is exactly what it is. Your Framer component then handles rendering ## as a header, • lines as indented items, and backtick spans as highlighted chips. Keeps the JSON readable and gives you something predictable to parse on the component side.
Step 5 — The Framer component
This is where everything becomes visible. The Framer Code component fetches CHANGELOG.json directly from GitHub on every page load, so the site is always current without a rebuild or redeploy. The moment the workflow commits a new entry, the next visitor sees it.
The raw URL it points to:
The component I built renders a timeline layout: a vertical line running down the left edge, with a coloured dot per entry that maps to the release type — green for launch, blue for update, orange for fix, purple for experiment, grey for meta. Each entry is a card with the type badge, project name (linked to the repo), version number (linked to the release), date, title, and the parsed description below.
The description parser handles the format from Step 4 directly: ## lines become uppercase section headers with a divider, - lines become indented items with a bullet indicator, and any text wrapped in backticks becomes an inline code chip. The rest renders as plain text. No markdown library involved — just string parsing that matches the format you're already writing in the release notes.
The whole thing is configurable through Framer's property controls panel without touching the code: filter by type or project, cap the number of entries, toggle date / type badge / project / version / emoji individually, adjust text colour, muted colour, card background, gap, and font family. Which means you can drop the same component on different pages with different configurations — a filtered view for one project, a full feed elsewhere.
The design itself (the card shape, the timeline line, the dot treatment, the badge style, the typography scale) came from scratch, not a template. If you're building your own version, the component is MIT-licensed and on GitHub above — copy it, strip it, make it yours.
The Claude Code piece
The last part is making sure Claude actually writes the description in the right format, every time, without being reminded.
Without a rule in place, Claude would write whatever felt natural for a release — sometimes a proper changelog, sometimes a summary, sometimes a link to the full notes. None of that parses cleanly in the Framer component.
The fix is a global rule in ~/.claude/CLAUDE.md:
Global means it applies across every project. Cut a release for anything, and Claude already knows the format — you never have to specify it again.
A few things that bit me during the build — worth knowing before you start:
⚠️ YAML via Python — use raw strings
'\n'in a regular Python string writes a literal newline into YAML block scalars, which breaks JS string literals inside. Always user"""..."""when building YAML content in Python.
⚠️ Bash heredoc + backticks = command substitution
Backticks inside a heredoc get executed as shell commands. If your content has backticks — JS template literals, markdown code spans — write to a temp
.pyfile and run it instead.
⚠️ GitHub Actions secrets are snapshot-baked
Secrets are captured at the first trigger. Adding a new secret and rerunning the old job won't pick it up. You need to trigger a fresh run to see new secrets.
That's the whole thing
Two workflow files, one JSON file, one PAT, one rule in CLAUDE.md. Nothing particularly exotic.
What I like about it is that the changelog is the source of truth for all three destinations at once: the Git tag, the GitHub release page, and the live site. You write the notes once, in the terminal, and they land everywhere. The portfolio stops being something you have to remember to update — it just reflects what's actually happening.
If you build something on top of this or find a better approach to the description format, let me know.
See you next time.
Reda.
