From beaf6ebf69cd20e7f9e891af6f1785fec91d464c Mon Sep 17 00:00:00 2001 From: Gagan Date: Mon, 6 Jul 2026 16:17:26 +0530 Subject: [PATCH] fix(apps): bootstrap empty repos before the Git Data push MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit GitHub's Git Data API (blobs/trees/commits) returns 409 'Git Repository is empty' on a repo with no commits — it cannot create the first commit. Ensure main exists via a Contents API bootstrap commit (the generated README), then chain the publish commit onto it. --- apps/x/packages/core/src/apps/publisher.ts | 30 +++++++++++++--------- 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/apps/x/packages/core/src/apps/publisher.ts b/apps/x/packages/core/src/apps/publisher.ts index 9f6dba49..3c545b65 100644 --- a/apps/x/packages/core/src/apps/publisher.ts +++ b/apps/x/packages/core/src/apps/publisher.ts @@ -165,6 +165,23 @@ async function pushSource(token: string, login: string, repoName: string, folder const dir = appDir(folder); const files = await collectSourceFiles(dir); + // The Git Data API (blobs/trees/commits) answers 409 "Git Repository is + // empty" on a repo with no commits — it cannot bootstrap an empty repo. + // Ensure main exists first via the Contents API, then chain onto it. + let parents: string[] = []; + try { + const ref = await gh<{ object: { sha: string } }>(token, 'GET', `/repos/${login}/${repoName}/git/ref/heads/main`); + parents = [ref.object.sha]; + } catch { + await gh(token, 'PUT', `/repos/${login}/${repoName}/contents/README.md`, { + message: 'bootstrap', + content: Buffer.from(generatedReadme(manifest)).toString('base64'), + branch: 'main', + }); + const ref = await gh<{ object: { sha: string } }>(token, 'GET', `/repos/${login}/${repoName}/git/ref/heads/main`); + parents = [ref.object.sha]; + } + type TreeEntry = { path: string; mode: '100644'; type: 'blob'; sha: string }; const tree: TreeEntry[] = []; for (const rel of files) { @@ -189,24 +206,13 @@ async function pushSource(token: string, login: string, repoName: string, folder const treeRes = await gh<{ sha: string }>(token, 'POST', `/repos/${login}/${repoName}/git/trees`, { tree }); - // Parent-less commit for an empty repo; otherwise chain onto main. - let parents: string[] = []; - try { - const ref = await gh<{ object: { sha: string } }>(token, 'GET', `/repos/${login}/${repoName}/git/ref/heads/main`); - parents = [ref.object.sha]; - } catch { /* empty repo */ } - const commit = await gh<{ sha: string }>(token, 'POST', `/repos/${login}/${repoName}/git/commits`, { message: `Publish ${manifest.name} v${version}`, tree: treeRes.sha, parents, }); - if (parents.length) { - await gh(token, 'PATCH', `/repos/${login}/${repoName}/git/refs/heads/main`, { sha: commit.sha, force: false }); - } else { - await gh(token, 'POST', `/repos/${login}/${repoName}/git/refs`, { ref: 'refs/heads/main', sha: commit.sha }); - } + await gh(token, 'PATCH', `/repos/${login}/${repoName}/git/refs/heads/main`, { sha: commit.sha, force: false }); } async function uploadAsset(token: string, uploadUrlBase: string, name: string, data: Buffer, contentType: string): Promise {