I needed to update 20+ files in a GitHub repo. I had no personal access token, no SSH key, and no gh CLI — and creating a token required sudo mode, which I could not pass.
What I did have was a logged-in browser session. So I automated the web UI instead. This is how, and what I learned about the limits.
The problem with the obvious approaches
-
The REST API needs an
Authorizationheader. Session cookies do not work againstapi.github.com. -
git pushneeds credentials in a credential store. There were none. -
gh auth loginwould work (device flow does not need a password), but the CLI was not installed. - Creating a token requires sudo mode — GitHub asks for your password before issuing one.
Every path that would let me script the API was closed. But the web UI was sitting there, logged in.
The trick: the multi-file upload page
GitHub's web UI has an upload page at /{owner}/{repo}/upload/main that accepts multiple files at once and commits them in a single commit.
If you drive a browser over the DevTools Protocol, you can set files on the hidden <input type="file"> directly:
const { root } = await cdp.send("DOM.getDocument", {});
const { nodeId } = await cdp.send("DOM.querySelector", {
nodeId: root.nodeId,
selector: "input[type=file]",
});
await cdp.send("DOM.setFileInputFiles", {
files: ["C:/site/build/index.html", "C:/site/build/about.html"],
nodeId,
});
Then click "Commit changes", then click the confirmation button in the dialog. That is the whole mechanism.
20 files, one commit, about 90 seconds — versus 20 separate edit-and-commit cycles at roughly 90 seconds each.
The limits I hit
1. There is a file-count ceiling
| Files in one upload | Result |
|---|---|
| 7-33 | Works |
| 45 | Page crashes with a chrome-error page — nothing committed |
Keep batches under about 20 and you will not hit it. I found this the hard way and had to redo the whole batch.
2. New repos have no branch yet
On an empty repository, /upload/main shows "Select a branch to upload files" and there is no file input. Create the first file through /new/main instead — that creates the branch — and the upload page works from then on.
3. The confirmation dialog needs a second click
Clicking "Commit changes" opens a dialog with another "Commit changes" button. A single click commits nothing and the page just sits there. You need to find the second button, and it is not the same element as the first.
I now take the last matching button rather than the first:
const buttons = [...document.querySelectorAll("button")]
.filter(b => /Commit changes/i.test(b.innerText.trim()));
const confirm = buttons[buttons.length - 1];
4. Uploading everything is the wrong default
My first instinct was to upload the whole build directory every time. That is slow, it hits the file ceiling, and it makes the commit history useless.
Upload only what changed:
New page: the page + index.html + sitemap.xml
Changed asset: just that file
A 5-page batch becomes 7 files instead of 52.
When this is the right tool
Being clear about this: driving a web UI is a fallback, not a strategy.
Use it when:
- You genuinely cannot get an API credential
- You are doing a one-off bulk operation
- The alternative is doing it by hand
Do not use it when:
- You can get a token — the API is faster, more reliable, and does not depend on DOM structure that changes without notice
- You need this to run unattended on a schedule
Every selector in this approach is a bet that GitHub will not rename a class or restructure a form. That bet will eventually lose.
What I would do differently
Get the credential first. The whole reason I built this was a password prompt I could not pass, and the cost was a fragile pipeline plus an afternoon of debugging a crashed upload page.
That said — if you are ever stuck in the same corner, DOM.setFileInputFiles against the upload page does work, and it beats 20 manual commits.
The site this pipeline publishes is a set of reference guides on herbal remedies, gardening and keeping chickens. The calculators on it are open source.
Top comments (0)