Skip to content

Commit 063346e

Browse files
feat: drop dependency on gh (#1000)
<!-- 👋 Hi, thanks for sending a PR to create-typescript-app! 💖. Please fill out all fields below and make sure each item is true and [x] checked. Otherwise we may not be able to review your PR. --> ## PR Checklist - [x] Addresses an existing open issue: fixes #667 - [x] That issue was marked as [`status: accepting prs`](https://github.com/JoshuaKGoldberg/create-typescript-app/issues?q=is%3Aopen+is%3Aissue+label%3A%22status%3A+accepting+prs%22) - [x] Steps in [CONTRIBUTING.md](https://github.com/JoshuaKGoldberg/create-typescript-app/blob/main/.github/CONTRIBUTING.md) were taken ## Overview Replaces the end-user dependency on `gh` (the GitHub CLI) with more manual Octokit calls. It's a bit less convenient this way but is more type-safe and means users don't need to have some random GitHub utility installed to use the template. Update December 2023: this works but I don't like how the user has to either have `gh` logged in or use a `process.env.GH_TOKEN`... I'd like to find the time to look into alternatives to log the user in. Update August 2024: I don't want to procrastinate any more. This PR doesn't make things _worse_. So as long as error messages explicitly tell people to either log in with `gh` or set `process.env.GH_TOKEN`, this is fine.
1 parent 6f260c8 commit 063346e

18 files changed

+406
-323
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,9 @@ It includes options not just for building and testing but also GitHub repository
2424

2525
First make sure you have the following installed:
2626

27-
- [GitHub CLI](https://cli.github.com) _(you'll need to be logged in)_
2827
- [Node.js](https://nodejs.org)
2928
- [pnpm](https://pnpm.io)
29+
- _(optional, but helpful)_ [GitHub CLI](https://cli.github.com) _(you'll need to be logged in)_
3030

3131
Then in an existing repository or in your directory where you'd like to make a new repository:
3232

src/create/createWithOptions.test.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,10 @@ describe("createWithOptions", () => {
9393
};
9494

9595
await createWithOptions({ github, options });
96-
expect(addToolAllContributors).toHaveBeenCalledWith(options);
96+
expect(addToolAllContributors).toHaveBeenCalledWith(
97+
github.octokit,
98+
options,
99+
);
97100
});
98101

99102
it("does not call addToolAllContributors when excludeAllContributors is true", async () => {

src/create/createWithOptions.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ export async function createWithOptions({ github, options }: GitHubAndOptions) {
3131

3232
if (!options.excludeAllContributors && !options.skipAllContributorsApi) {
3333
await withSpinner("Adding contributors to table", async () => {
34-
await addToolAllContributors(options);
34+
await addToolAllContributors(github?.octokit, options);
3535
});
3636
}
3737

src/initialize/initializeWithOptions.test.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,10 @@ describe("initializeWithOptions", () => {
8181
options,
8282
});
8383

84-
expect(mockAddOwnerAsAllContributor).toHaveBeenCalledWith(options);
84+
expect(mockAddOwnerAsAllContributor).toHaveBeenCalledWith(
85+
undefined,
86+
options,
87+
);
8588
});
8689

8790
it("does not run addOwnerAsAllContributor when excludeAllContributors is true", async () => {

src/initialize/initializeWithOptions.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ export async function initializeWithOptions({
4141

4242
if (!options.excludeAllContributors) {
4343
await withSpinner("Updating existing contributor details", async () => {
44-
await addOwnerAsAllContributor(options);
44+
await addOwnerAsAllContributor(github?.octokit, options);
4545
});
4646
}
4747

src/shared/getGitHubUserAsAllContributor.test.ts

Lines changed: 23 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import chalk from "chalk";
2+
import { Octokit } from "octokit";
23
import { beforeEach, describe, expect, it, MockInstance, vi } from "vitest";
34

45
import { getGitHubUserAsAllContributor } from "./getGitHubUserAsAllContributor.js";
@@ -13,6 +14,11 @@ vi.mock("execa", () => ({
1314

1415
let mockConsoleWarn: MockInstance;
1516

17+
const createMockOctokit = (getAuthenticated: MockInstance = vi.fn()) =>
18+
({
19+
rest: { users: { getAuthenticated } },
20+
}) as unknown as Octokit;
21+
1622
const owner = "TestOwner";
1723

1824
describe("getGitHubUserAsAllContributor", () => {
@@ -23,7 +29,8 @@ describe("getGitHubUserAsAllContributor", () => {
2329
});
2430

2531
it("defaults to owner with a log when options.offline is true", async () => {
26-
const actual = await getGitHubUserAsAllContributor({
32+
const octokit = createMockOctokit();
33+
const actual = await getGitHubUserAsAllContributor(octokit, {
2734
offline: true,
2835
owner,
2936
});
@@ -36,23 +43,24 @@ describe("getGitHubUserAsAllContributor", () => {
3643
);
3744
});
3845

46+
it("defaults to owner without a log when octokit is undefined", async () => {
47+
const actual = await getGitHubUserAsAllContributor(undefined, { owner });
48+
49+
expect(actual).toEqual(owner);
50+
expect(mockConsoleWarn).not.toHaveBeenCalled();
51+
});
52+
3953
it("uses the user from gh api user when it succeeds", async () => {
4054
const login = "gh-api-user";
55+
const octokit = createMockOctokit(
56+
vi.fn().mockResolvedValue({ data: { login } }),
57+
);
4158

42-
mock$.mockResolvedValueOnce({
43-
stdout: JSON.stringify({ login }),
44-
});
45-
46-
await getGitHubUserAsAllContributor({ owner });
59+
await getGitHubUserAsAllContributor(octokit, { owner });
4760

4861
expect(mockConsoleWarn).not.toHaveBeenCalled();
4962
expect(mock$.mock.calls).toMatchInlineSnapshot(`
5063
[
51-
[
52-
[
53-
"gh api user",
54-
],
55-
],
5664
[
5765
[
5866
"npx -y [email protected] add ",
@@ -67,9 +75,11 @@ describe("getGitHubUserAsAllContributor", () => {
6775
});
6876

6977
it("defaults the user to the owner when gh api user fails", async () => {
70-
mock$.mockRejectedValueOnce({});
78+
const octokit = createMockOctokit(
79+
vi.fn().mockRejectedValue(new Error("Oh no!")),
80+
);
7181

72-
await getGitHubUserAsAllContributor({ owner });
82+
await getGitHubUserAsAllContributor(octokit, { owner });
7383

7484
expect(mockConsoleWarn).toHaveBeenCalledWith(
7585
chalk.gray(
@@ -78,11 +88,6 @@ describe("getGitHubUserAsAllContributor", () => {
7888
);
7989
expect(mock$.mock.calls).toMatchInlineSnapshot(`
8090
[
81-
[
82-
[
83-
"gh api user",
84-
],
85-
],
8691
[
8792
[
8893
"npx -y [email protected] add ",

src/shared/getGitHubUserAsAllContributor.ts

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,11 @@
11
import chalk from "chalk";
22
import { $ } from "execa";
3+
import { Octokit } from "octokit";
34

45
import { Options } from "./types.js";
56

6-
interface GhUserOutput {
7-
login: string;
8-
}
9-
107
export async function getGitHubUserAsAllContributor(
8+
octokit: Octokit | undefined,
119
options: Pick<Options, "offline" | "owner">,
1210
) {
1311
if (options.offline) {
@@ -21,14 +19,18 @@ export async function getGitHubUserAsAllContributor(
2119

2220
let user: string;
2321

24-
try {
25-
user = (JSON.parse((await $`gh api user`).stdout) as GhUserOutput).login;
26-
} catch {
27-
console.warn(
28-
chalk.gray(
29-
`Couldn't authenticate GitHub user, falling back to the provided owner name '${options.owner}'.`,
30-
),
31-
);
22+
if (octokit) {
23+
try {
24+
user = (await octokit.rest.users.getAuthenticated()).data.login;
25+
} catch {
26+
console.warn(
27+
chalk.gray(
28+
`Couldn't authenticate GitHub user, falling back to the provided owner name '${options.owner}'.`,
29+
),
30+
);
31+
user = options.owner;
32+
}
33+
} else {
3234
user = options.owner;
3335
}
3436

src/shared/options/createRepositoryWithApi.ts

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -22,14 +22,20 @@ export async function createRepositoryWithApi(
2222

2323
const currentUser = await octokit.rest.users.getAuthenticated();
2424

25-
if (currentUser.data.login === options.owner) {
26-
await octokit.rest.repos.createForAuthenticatedUser({
27-
name: options.repository,
28-
});
29-
} else {
30-
await octokit.rest.repos.createInOrg({
31-
name: options.repository,
32-
org: options.owner,
25+
try {
26+
if (currentUser.data.login === options.owner) {
27+
await octokit.rest.repos.createForAuthenticatedUser({
28+
name: options.repository,
29+
});
30+
} else {
31+
await octokit.rest.repos.createInOrg({
32+
name: options.repository,
33+
org: options.owner,
34+
});
35+
}
36+
} catch (error) {
37+
throw new Error("Failed to create new repository on GitHub.", {
38+
cause: error,
3339
});
3440
}
3541
}

src/shared/options/getGitHub.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ describe("getOctokit", () => {
3232
});
3333

3434
await expect(getGitHub).rejects.toMatchInlineSnapshot(
35-
"[Error: GitHub authentication failed.]",
35+
`[Error: Couldn't authenticate with GitHub. Either log in with \`gh auth login\` (https://cli.github.com) or set a GH_TOKEN environment variable.]`,
3636
);
3737
});
3838

src/shared/options/getGitHub.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,10 @@ export async function getGitHub(): Promise<GitHub | undefined> {
1010
const auth = await getGitHubAuthToken();
1111

1212
if (!auth.succeeded) {
13-
throw new Error("GitHub authentication failed.", {
14-
cause: auth.error,
15-
});
13+
throw new Error(
14+
"Couldn't authenticate with GitHub. Either log in with `gh auth login` (https://cli.github.com) or set a GH_TOKEN environment variable.",
15+
{ cause: auth.error },
16+
);
1617
}
1718

1819
const octokit = new Octokit({ auth: auth.token });

0 commit comments

Comments
 (0)