Dan Matthew
Service

Operator test

System
Nimbus CMS
Skin
Attract mode (arcade)
Entries
on this screen
Menus
8 items
Path
/blog/packkit-create-repo-clean-first-commit-from-the-browser
Credits
Free play
Press ~ to toggle · Esc to close

Writing · 15 Aug 2026

Creating a GitHub repo and pushing one clean commit, from a browser tab

I wanted the browser configurator to create your new repo and push the scaffold as a single clean commit. It worked, after a misleading 404 sent me chasing the wrong bug for a day.

#packkit #github #oauth #cloudflare #debugging

The Packkit web configurator could already build a project in your browser and hand you a zip. That is fine, but it leaves you with the boring part: make a repo on GitHub, git init, add a remote, push. I wanted the button to do it. Click "Create GitHub repo", authorize, and land on a fresh repository with the whole scaffold already in it as the first commit.

This is the story of building that, and of the day I lost to a 404 that was lying to me.

What it is

A small OAuth flow plus a push, running entirely in Cloudflare Pages Functions next to the static site, so there is no separate server. You click the button, GitHub asks if you consent, and the configurator creates the repo under your account and pushes every generated file as one commit titled "Initial commit from Packkit". The token never touches the browser. It lives server side in an httpOnly, single-use cookie that is cleared the moment the push finishes.

The one rule I set for myself: the new repo gets exactly one clean commit. Not an auto-created README followed by mine. Not a commit per file. One.

The one clean commit is harder than it sounds

The obvious way to push files through the GitHub API is the Contents API, one PUT per file. That works and it is a trap: each PUT is its own commit, so a 30 file scaffold gives you a 30 commit history before you have written a line of code. It also serializes, so it is slow.

The right way is the Git Data API, which lets you assemble a commit out of parts. You create a blob for each file in parallel, build one tree from all the blob SHAs, create a single commit that points at that tree, then move the branch to it. The catch is that a brand new empty repository has no object to hang a tree on, and the API rejects you. So I create the repo with auto_init: true, which seeds it with a README, then I make my commit an orphan, a commit with no parents, and force the default branch onto it. The auto-init README ends up orphaned and invisible. History is exactly one commit, which is what I promised.

The 404 that was lying

Then it stopped working, and the way it failed sent me down a hole.

The push kept dying with tree_failed: Not Found. A 404. My first assumption was the empty-repo problem, so I added the auto_init and a readiness poll. Still 404. Then I convinced myself there was a size limit, because the failure seemed to land around 14 files. I even had a log line that read batch@10 n=5 cum=15: Not Found, which looked exactly like a threshold. I started batching the tree differently to get under it.

None of that was the bug.

Here is what actually happened. Every Packkit scaffold includes a .github/workflows/ci.yml. Writing a file under .github/workflows/ through the GitHub API requires the OAuth workflow scope, on top of repo. I had asked for repo only. And when GitHub refuses a workflow write for a missing scope, it does not return a 403 that says "you need the workflow scope". It returns a 404. Not Found. The "size limit at 14 files" was a coincidence: the batch that happened to contain ci.yml was the one that failed, every time.

What kept me blind to it was my own testing. My local reproductions, run with a personal access token, always passed, because a classic PAT with repo can write workflows. The browser flow, with an OAuth app scoped to repo, could not. Same code, different token, and the only difference was a scope I did not know that path needed.

The fix was one word. The authorize step now asks for repo workflow, and the clean Git Data push works exactly as designed. Verified on a real repo: 32 files, one commit, ci.yml present.

The lesson I wrote down

I saved this one to memory so a future version of me does not lose the same day: if you ever get a 404 writing to a repo you can clearly push to, before you believe the 404, check whether the path is under .github/workflows/ and whether your token has the workflow scope. GitHub reports that specific permission failure as Not Found, and it will happily let you chase a phantom size limit for hours.

What is still rough

The token is single-use and short-lived by design, which is the safe choice but means a failed push needs a fresh authorize, not a retry. And a repo this new has no lockfile yet, so its own CI fails on the very first run with a missing-lockfile error until you clone, install, and commit the lock. That one is not a bug, it is physics, but it surprised enough people that the generated README now explains it in plain words instead of leaving them to read a red X.

The flow is live on the configurator at packkit-web.pages.dev. Pick a stack, click Create GitHub repo, choose public or private, and you land on the new repository with the scaffold already committed.