What if a playlist worked like a repository?
You'd propose a song with a pull request. CI would check that it's a real track and not a duplicate. Review would mean listening to it. Merging would publish it. And git log would tell you who added every song, and why.
So I built that. It's two repos:
- 🎧 Open Source Mixtape: a public Spotify playlist that anyone with a GitHub account can add to. Listen on Spotify.
- 🛠 spotify-playlist-as-code: the template it runs on. Click Use this template and your own playlist takes PRs too.
No server, no database, no dependencies beyond the Python standard library. Just a text file, GitHub Actions, and the Spotify Web API.
The whole playlist is one text file
That's it. One line per song, in playlist order. The link can be a spotify:track: URI, an open.spotify.com link (with or without ?si=… or /intl-ja/), or a bare 22-character ID. The # Artist - Title after it is for humans, so reviewers know what they're about to hear.
The # @playlist line says which Spotify playlist the file syncs to. A file without it still gets validated, it just never touches Spotify, which is handy for trying things out.
From a pasted link to a live playlist
Asking strangers to fork a repo and edit a file to suggest a song is a lot. So the front door is an issue form that asks for a single thing: a Spotify link.
- Someone opens a "🎵 Add a song" issue and pastes a link. "Why this one?" and "Listening from" are optional.
-
A workflow turns the issue into a PR. It looks the song up on Spotify, appends
<link> # Artist - Titleto the file, and comments back on the issue with the PR link. If the link is an album, or the song is already in the playlist, it says so on the issue and closes it. -
CI validates the PR. Malformed IDs and duplicates fail the check with an annotation on the exact line. Every added track gets looked up, and the job summary reads like a tracklist:
➕ KiiiKiii - Pop Off Pop Off. - Review is listening. I play the song, then merge, comment, or politely pass.
-
Merging is publishing. A push to
mainthat touchesplaylists/runs the sync, and the Spotify playlist now matches the file.
Five things I learned building it
1. Make the sync declarative
The sync doesn't figure out "add this, remove that". It replaces the playlist with whatever the file says:
def replace_playlist(self, playlist_id: str, uris: list[str]) -> None:
"""Make the playlist contain exactly `uris`, in order."""
first, rest = uris[:BATCH], uris[BATCH:]
self.request("PUT", f"/playlists/{playlist_id}/items", {"uris": first})
for i in range(0, len(rest), BATCH):
self.request("POST", f"/playlists/{playlist_id}/items", {"uris": rest[i : i + BATCH]})
PUT sets the first 100 tracks and POST appends the rest in batches of 100. Reorders, removals and reverts all come for free, and a failed run is fixed by just running it again. The trade-off is in the README in a big warning box: anything added straight in the Spotify app disappears on the next sync. The file wins. That's the point.
(If you've used the Spotify API before: these are the /items endpoints from the February 2026 Web API changes. The old /tracks paths are gone.)
2. PRs opened by GITHUB_TOKEN don't trigger your other workflows
This one cost me a while. The bot opens its PR with the built-in GITHUB_TOKEN, and GitHub deliberately doesn't start new workflow runs from events caused by that token, so on: pull_request never fired and the bot's PRs had no check at all.
The fix is to give the validate workflow a workflow_dispatch trigger and have the bot start it by hand:
url=$(gh pr create --base "$DEFAULT_BRANCH" --head "$branch" \
--title "Add $LABEL" --body-file "$PR_BODY_FILE")
# Pull requests opened with GITHUB_TOKEN don't trigger other workflows, so start the check here.
gh workflow run validate.yml --ref "$branch"
You also need Settings → Actions → General → Allow GitHub Actions to create and approve pull requests, or gh pr create fails.
3. Give the human credit, not the bot
The commit is made by github-actions[bot], but the song came from whoever opened the issue. So the commit message carries a co-author trailer:
git commit -m "Add $LABEL" -m "Suggested in #$NUMBER." \
-m "Co-authored-by: $AUTHOR <$AUTHOR_ID+$AUTHOR@users.noreply.github.com>"
With the ID+login@users.noreply.github.com form, GitHub links the commit to their profile. Adding a song to a playlist shows up as a real contribution.
4. Git history makes great liner notes
I didn't build any of this. git log -p playlists/ already answers "who added this song, when, and why", and git revert undoes a bad pick. Version control turns out to be a pretty good music library.
5. Plan for forks without secrets
Pull requests from forks don't get repository secrets, so CI can't call Spotify for them. Rather than failing, the validator skips the online lookup and still checks the format and duplicates. The sync does the same: no secrets, so it runs as a dry run. A fresh copy of the template is green on day one before you've set anything up.
Make your own
Want this for your team's office playlist, a party queue, or a road trip with friends?
- Click Use this template on spotify-playlist-as-code.
- Create a Spotify app, get a refresh token with
python scripts/get_refresh_token.py, and add three repository secrets. - Put your playlist link in a
# @playlistline and merge.
The full steps are in the README. One heads-up: Spotify's development mode needs the app owner to have Premium. People adding songs don't need it.
Add a song to the mixtape 🎶
The mixtape is small right now, so your pick will stand out. Any genre, any language, any decade.
👉 Open an "Add a song" issue and paste a Spotify link. The bot does the rest.
Then tell me in the comments what you added and why. And if you have an idea for the tool itself, issues and PRs are welcome on spotify-playlist-as-code. ⭐ Stars help other people find it.



Top comments (1)
Okay, I'll go first 🎶
If you could add one song to the mixtape, what would it be? Any genre, any language, any decade. I'm especially curious about songs most people haven't heard yet.
Drop it here, or open an "Add a song" issue and the bot will open the PR for you. I'll listen to every one.