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

+1-1
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

+4-1
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

+1-1
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

+4-1
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

+1-1
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

+23-18
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

+14-12
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

+14-8
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

+1-1
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

+4-3
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 });

src/steps/addOwnerAsAllContributor.test.ts

+4-4
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ describe("addOwnerAsAllContributor", () => {
4141
mockReadFileAsJson.mockResolvedValue("invalid");
4242

4343
await expect(async () => {
44-
await addOwnerAsAllContributor({ owner: mockOwner });
44+
await addOwnerAsAllContributor(undefined, { owner: mockOwner });
4545
}).rejects.toMatchInlineSnapshot(
4646
'[Error: Invalid .all-contributorsrc: "invalid"]',
4747
);
@@ -54,7 +54,7 @@ describe("addOwnerAsAllContributor", () => {
5454
mockReadFileAsJson.mockResolvedValue({});
5555

5656
await expect(async () => {
57-
await addOwnerAsAllContributor({ owner: mockOwner });
57+
await addOwnerAsAllContributor(undefined, { owner: mockOwner });
5858
}).rejects.toMatchInlineSnapshot(
5959
"[Error: Invalid .all-contributorsrc: {}]",
6060
);
@@ -68,7 +68,7 @@ describe("addOwnerAsAllContributor", () => {
6868
contributors: [],
6969
});
7070

71-
await addOwnerAsAllContributor({ owner: mockOwner });
71+
await addOwnerAsAllContributor(undefined, { owner: mockOwner });
7272

7373
expect(mockWriteFile).toHaveBeenCalledWith(
7474
"./.all-contributorsrc",
@@ -89,7 +89,7 @@ describe("addOwnerAsAllContributor", () => {
8989
],
9090
});
9191

92-
await addOwnerAsAllContributor({ owner: mockOwner });
92+
await addOwnerAsAllContributor(undefined, { owner: mockOwner });
9393

9494
expect(mockWriteFile).toHaveBeenCalledWith(
9595
"./.all-contributorsrc",

src/steps/addOwnerAsAllContributor.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,16 @@
11
import * as fs from "node:fs/promises";
2+
import { Octokit } from "octokit";
23

34
import { getGitHubUserAsAllContributor } from "../shared/getGitHubUserAsAllContributor.js";
45
import { readFileAsJson } from "../shared/readFileAsJson.js";
56
import { AllContributorsData, Options } from "../shared/types.js";
67
import { formatJson } from "./writing/creation/formatters/formatJson.js";
78

89
export async function addOwnerAsAllContributor(
10+
octokit: Octokit | undefined,
911
options: Pick<Options, "offline" | "owner">,
1012
) {
11-
const user = await getGitHubUserAsAllContributor(options);
13+
const user = await getGitHubUserAsAllContributor(octokit, options);
1214

1315
const existingContributors = (await readFileAsJson(
1416
"./.all-contributorsrc",

src/steps/addToolAllContributors.test.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -22,15 +22,15 @@ describe("addToolAllContributors", () => {
2222
it("adds JoshuaKGoldberg when that is not the current github user", async () => {
2323
mockGetGitHubUserAsAllContributor.mockResolvedValue("JoshuaKGoldberg");
2424

25-
await addToolAllContributors({ owner: "owner" });
25+
await addToolAllContributors(undefined, { owner: "owner" });
2626

2727
expect(mock$).not.toHaveBeenCalled();
2828
});
2929

3030
it("does not add JoshuaKGoldberg when that not the current github user", async () => {
3131
mockGetGitHubUserAsAllContributor.mockResolvedValue("other");
3232

33-
await addToolAllContributors({ owner: "owner" });
33+
await addToolAllContributors(undefined, { owner: "owner" });
3434

3535
expect(mock$).toHaveBeenCalledWith([
3636
`npx -y all-contributors-cli add JoshuaKGoldberg tool`,

src/steps/addToolAllContributors.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
import { $ } from "execa";
2+
import { Octokit } from "octokit";
23

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

67
export async function addToolAllContributors(
8+
octokit: Octokit | undefined,
79
options: Pick<Options, "offline" | "owner">,
810
) {
9-
const login = await getGitHubUserAsAllContributor(options);
11+
const login = await getGitHubUserAsAllContributor(octokit, options);
1012

1113
if (login !== "JoshuaKGoldberg") {
1214
await $`npx -y all-contributors-cli add JoshuaKGoldberg tool`;

src/steps/initializeGitHubRepository/index.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -13,5 +13,5 @@ export async function initializeGitHubRepository(
1313
await initializeGitRemote(options);
1414
await initializeRepositorySettings(octokit, options);
1515
await initializeBranchProtectionSettings(octokit, options);
16-
await initializeRepositoryLabels();
16+
await initializeRepositoryLabels(octokit, options);
1717
}

src/steps/initializeGitHubRepository/labels/getExistingEquivalentLabels.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ const aliases = new Map([
55

66
export interface GhLabelData {
77
color: string;
8-
description: string;
8+
description: null | string;
99
name: string;
1010
}
1111

0 commit comments

Comments
 (0)