Skip to content

Commit d0d05bb

Browse files
fix(cli): handle empty GitHub repositories during SDK generation (#10365)
* fix(cli): handle empty GitHub repositories during SDK generation --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Co-authored-by: Niels Swimberghe <[email protected]>
1 parent 93c0465 commit d0d05bb

File tree

5 files changed

+68
-15
lines changed

5 files changed

+68
-15
lines changed

packages/commons/github/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
},
3535
"dependencies": {
3636
"octokit": "^4.1.4",
37+
"simple-git": "^3.24.0",
3738
"tmp-promise": "^3.0.3"
3839
},
3940
"devDependencies": {

packages/commons/github/src/ClonedRepository.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,30 @@ export class ClonedRepository {
8686
await this.git.push();
8787
}
8888

89+
public async isRemoteEmpty(): Promise<boolean> {
90+
await this.git.cwd(this.clonePath);
91+
try {
92+
const result = await this.git.raw(["ls-remote", "--heads", "origin"]);
93+
return result.trim().length === 0;
94+
} catch (_error) {
95+
return true;
96+
}
97+
}
98+
99+
public async checkoutOrCreateLocal(branch: string): Promise<void> {
100+
await this.git.cwd(this.clonePath);
101+
try {
102+
await this.git.checkout(branch);
103+
} catch (_error) {
104+
await this.git.checkoutLocalBranch(branch);
105+
}
106+
}
107+
108+
public async pushUpstream(branch: string): Promise<void> {
109+
await this.git.cwd(this.clonePath);
110+
await this.git.push("origin", branch, { "--set-upstream": null });
111+
}
112+
89113
public async overwriteLocalContents(sourceDirectoryPath: string): Promise<void> {
90114
const [sourceContents, destContents] = await Promise.all([
91115
readdir(sourceDirectoryPath),

packages/generator-cli/src/github/GitHub.ts

Lines changed: 33 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { cwd, resolve } from "@fern-api/fs-utils";
2-
import { cloneRepository } from "@fern-api/github";
2+
import { cloneRepository, parseRepository } from "@fern-api/github";
33
import type { ClonedRepository } from "@fern-api/github/src/ClonedRepository";
44
import { Octokit } from "@octokit/rest";
55

@@ -27,16 +27,29 @@ export class GitHub {
2727
installationToken: this.githubConfig.token
2828
});
2929

30-
const branch = this.githubConfig.branch ?? (await repository.getDefaultBranch());
30+
const isEmptyRepo = await repository.isRemoteEmpty();
31+
32+
let branch: string;
33+
if (isEmptyRepo) {
34+
branch = this.githubConfig.branch ?? "main";
35+
await repository.checkoutOrCreateLocal(branch);
36+
} else {
37+
branch = this.githubConfig.branch ?? (await repository.getDefaultBranch());
38+
await repository.checkout(branch);
39+
await repository.pull(branch);
40+
}
3141

32-
await repository.checkout(branch);
33-
await repository.pull(branch);
3442
const fernIgnoreFiles = await this.getFernignoreFiles(repository);
3543
await repository.overwriteLocalContents(sourceDirectory);
3644
await repository.add(".");
3745
await this.restoreFiles(repository, fernIgnoreFiles);
3846
await repository.commit("SDK Generation");
39-
await repository.push();
47+
48+
if (isEmptyRepo) {
49+
await repository.pushUpstream(branch);
50+
} else {
51+
await repository.push();
52+
}
4053
} catch (error) {
4154
// TODO: migrate this to use @fern-api/logger
4255
console.error("Error during GitHub push:", error);
@@ -54,13 +67,23 @@ export class GitHub {
5467
installationToken: this.githubConfig.token
5568
});
5669

57-
const baseBranch = this.githubConfig.branch ?? (await repository.getDefaultBranch());
70+
const isEmptyRepo = await repository.isRemoteEmpty();
71+
72+
let baseBranch: string;
73+
if (isEmptyRepo) {
74+
baseBranch = this.githubConfig.branch ?? "main";
75+
await repository.checkoutOrCreateLocal(baseBranch);
76+
await repository.commit("Initial commit");
77+
await repository.pushUpstream(baseBranch);
78+
} else {
79+
baseBranch = this.githubConfig.branch ?? (await repository.getDefaultBranch());
80+
await repository.checkout(baseBranch);
81+
await repository.pull(baseBranch);
82+
}
5883

5984
const now = new Date();
6085
const formattedDate = now.toISOString().replace("T", "_").replace(/:/g, "-").replace(/\..+/, "");
6186
const prBranch = `fern-bot/${formattedDate}`;
62-
await repository.checkout(baseBranch);
63-
await repository.pull(baseBranch);
6487
await repository.checkout(prBranch);
6588

6689
const fernIgnoreFiles = await this.getFernignoreFiles(repository);
@@ -74,10 +97,8 @@ export class GitHub {
7497
auth: this.githubConfig.token
7598
});
7699
// Use octokit directly to create the pull request
77-
const [owner, repo] = this.githubConfig.uri.split("/");
78-
if (!owner || !repo) {
79-
throw new Error(`Invalid repository URI: ${this.githubConfig.uri}`);
80-
}
100+
const parsedRepo = parseRepository(this.githubConfig.uri);
101+
const { owner, repo } = parsedRepo;
81102
const head = `${owner}:${prBranch}`;
82103
try {
83104
await octokit.pulls.create({

packages/generator-cli/versions.yml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,11 @@
11
# yaml-language-server: $schema=../../versions-yml.schema.json
2+
- changelogEntry:
3+
- summary: |
4+
Fix an issue where the `generator-cli github pr ...` and `generator-cli github push` commands would fail when the GitHub repository is empty.
5+
type: fix
6+
createdAt: "2025-11-06"
7+
version: 0.4.2
8+
29
- changelogEntry:
310
- summary: |
411
Use proper title case in README generator. Conjunctions and prepositions are now lowercased when they appear in the middle of titles.

pnpm-lock.yaml

Lines changed: 3 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)