Repository: JoshuaKGoldberg/bingo (ts)

Issue:
🐛 Bug: Don't call to GitHub APIs when the running user isn't logged in
### Bug Report Checklist

- [x] I have tried restarting my IDE and the issue persists.
- [x] I have pulled the latest `main` branch of the repository.
- [x] I have [searched for related issues](https://github.com/JoshuaKGoldberg/bingo/issues?q=is%3Aissue) and found none that matched my issue.

### Expected

If the user isn't claiming to be logged into GitHub -i.e. has a `process.env.GH_TOKEN` per https://github.com/JoshuaKGoldberg/get-github-auth-token- then network requests to populate the GitHub repo shouldn't be sent. They certainly shouldn't have errors reported.

### Actual

They still are. Here's what happens if you run `create-typescript-app` logged out:

```shell
npx ~/repos/create-typescript-app --directory bingo-temp --description "temp" --owner temp --preset minimal
```

```plaintext
┌  ✨ create-typescript-app@2.4.6 ✨
│
●  Learn more on:
│    https://github.com/JoshuaKGoldberg/create-typescript-app
│
◇  Running with mode --setup using the template:
│    create-typescript-app
│
◇  Inferred default options from system
│
▲  Running in local-only mode without a repository on GitHub. To push to GitHub, log in with the GitHub CLI (cli.github.com) or run with a GH_TOKEN process environment variable.
│
◇  Ran the Create TypeScript App template
│
◇  Prepared local repository
│
│  You've got a new repository ready to use in:
│    ./bingo-temp
│
■  The branch-ruleset-create request failed. You should re-run it and fix its complaints.
│  HttpError: Not Found - https://docs.github.com/rest/repos/rules#create-a-repository-ruleset
│      at fetchWrapper (file:///Users/josh/repos/bingo/node_modules/.pnpm/@octokit+request@9.2.2/node_modules/@octokit/request/dist-bundle/index.js:122:11)
│      at process.processTicksAndRejections (node:internal/process/task_queues:105:5)
│      at async requestWithGraphqlErrorHandling (file:///Users/josh/repos/bingo/node_modules/.pnpm/@octokit+plugin-retry@7.1.4_@octokit+core@6.1.4/node_modules/@octokit/plugin-retry/dist-bundle/index.js:36:20)
│      at async Job.doExecute (/Users/josh/repos/bingo/node_modules/.pnpm/bottleneck@2.19.5/node_modules/bottleneck/light.js:405:18)
│
■  The repository-labels request failed. You should re-run it and fix its complaints.
│  Error: Please provide an auth token (process.env.GH_TOKEN) or log in with the GitHub CLI (gh).
│      at retrieveAuth (file:///Users/josh/repos/create-typescript-app/node_modules/.pnpm/octokit-from-auth@0.2.0/node_modules/octokit-from-auth/lib/index.js:18:9)
│      at process.processTicksAndRejections (node:internal/process/task_queues:105:5)
│      at async octokitFromAuth (file:///Users/josh/repos/create-typescript-app/node_modules/.pnpm/octokit-from-auth@0.2.0/node_modules/octokit-from-auth/lib/index.js:4:16)
│      at async setGitHubRepositoryLabels (file:///Users/josh/repos/create-typescript-app/node_modules/.pnpm/set-github-repository-labels@0.1.0/node_modules/set-github-repository-labels/lib/setGitHubRepositoryLabels.js:12:19)
│      at async Object.send (file:///Users/josh/repos/create-typescript-app/lib/blocks/blockRepositoryLabels.js:14:13)
│      at async file:///Users/josh/repos/bingo/packages/bingo/lib/runners/applyRequestsToSystem.js:5:13
│      at async Promise.all (index 1)
│      at async applyRequestsToSystem (file:///Users/josh/repos/bingo/packages/bingo/lib/runners/applyRequestsToSystem.js:2:5)
│      at async Promise.all (index 1)
│      at async runCreation (file:///Users/josh/repos/bingo/packages/bingo/lib/runners/runCreation.js:8:5)
│
■  The repository-settings request failed. You should re-run it and fix its complaints.
│  HttpError: Not Found - https://docs.github.com/rest/repos/repos#update-a-repository
│      at fetchWrapper (file:///Users/josh/repos/bingo/node_modules/.pnpm/@octokit+request@9.2.2/node_modules/@octokit/request/dist-bundle/index.js:122:11)
│      at process.processTicksAndRejections (node:internal/process/task_queues:105:5)
│      at async requestWithGraphqlErrorHandling (file:///Users/josh/repos/bingo/node_modules/.pnpm/@octokit+plugin-retry@7.1.4_@octokit+core@6.1.4/node_modules/@octokit/plugin-retry/dist-bundle/index.js:36:20)
│      at async Job.doExecute (/Users/josh/repos/bingo/node_modules/.pnpm/bottleneck@2.19.5/node_modules/bottleneck/light.js:405:18)
│
└  Thanks for using create-typescript-app! 💝
```

### Additional Info

I think this depends on #65. `requests` creations don't yet have a way to distinguish what areas of requests are sent to which service until they're run.

In theory, folks might want to use the serializable `octokit`-based requests to send to some GitHub endpoint other than the created repo's. This would make that more difficult for them. But I think that's a niche edge case that can be done as a followup issue. For example, there might be some kind of `always?: boolean` option that could be added to #65's request schema as a followup.

💝 

Pull request:
fix: skip calling to GitHub APIs without an auth token
## PR Checklist

- [x] Addresses an existing open issue: fixes #275
- [x] That issue was marked as [`status: accepting prs`](https://github.com/JoshuaKGoldberg/bingo/issues?q=is%3Aopen+is%3Aissue+label%3A%22status%3A+accepting+prs%22)
- [x] Steps in [CONTRIBUTING.md](https://github.com/JoshuaKGoldberg/bingo/blob/main/.github/CONTRIBUTING.md) were taken

## Overview

Changes `createSystemFetchers` & `SystemFetchers` to make `auth` -> `octokit` optional. `octokit` will only be created if auth is available. As a result, downstream:

* The Bingo CLI will then only attempt to create a repository if `!offline` and that `octokit` was able to create a repository
* `applyRequestsToSystem` won't attempt to make requests when an equivalent "sender" cannot be created: which is the case for `type: "octokit"` requests and no `octokit`

💝 

Patch:
diff --git a/packages/bingo-systems/src/fetchers/createSystemFetchers.ts b/packages/bingo-systems/src/fetchers/createSystemFetchers.ts
index 6a9b1fe..9a5d05a 100644
--- a/packages/bingo-systems/src/fetchers/createSystemFetchers.ts
+++ b/packages/bingo-systems/src/fetchers/createSystemFetchers.ts
@@ -13,9 +13,11 @@ export function createSystemFetchers({
 }: SystemFetchersSettings): SystemFetchers {
 	return {
 		fetch,
-		octokit: new Octokit({
-			auth,
-			request: { fetch },
-		}),
+		octokit: auth
+			? new Octokit({
+					auth,
+					request: { fetch },
+				})
+			: undefined,
 	};
 }
diff --git a/packages/bingo-systems/src/fetchers/types.ts b/packages/bingo-systems/src/fetchers/types.ts
index 15e6b62..a9a2545 100644
--- a/packages/bingo-systems/src/fetchers/types.ts
+++ b/packages/bingo-systems/src/fetchers/types.ts
@@ -11,8 +11,8 @@ export interface SystemFetchers {
 	fetch: typeof fetch;
 
 	/**
-	 * Octokit.js instance wrapping the same `fetch` function.
+	 * Octokit.js instance wrapping the same `fetch` function, if authenticated.
 	 * @see {@link https://octokit.github.io/rest.js}
 	 */
-	octokit: Octokit;
+	octokit: Octokit | undefined;
 }
diff --git a/packages/bingo/src/cli/setup/createRepositoryOnGitHub.ts b/packages/bingo/src/cli/setup/createRepositoryOnGitHub.ts
index 8d51497..c2cdd2d 100644
--- a/packages/bingo/src/cli/setup/createRepositoryOnGitHub.ts
+++ b/packages/bingo/src/cli/setup/createRepositoryOnGitHub.ts
@@ -63,7 +63,7 @@ export async function createRepositoryOnGitHub<
 		(await promptForOptionSchema(
 			"owner",
 			template.options.owner,
-			"GitHub organization or user the repository is underneath",
+			"organization or username owning the repository",
 			undefined,
 		));
 
diff --git a/packages/bingo/src/cli/setup/runModeSetup.ts b/packages/bingo/src/cli/setup/runModeSetup.ts
index 0b281b0..7b6970c 100644
--- a/packages/bingo/src/cli/setup/runModeSetup.ts
+++ b/packages/bingo/src/cli/setup/runModeSetup.ts
@@ -112,15 +112,16 @@ export async function runModeSetup<OptionsShape extends AnyShape, Refinements>({
 		return { status: CLIStatus.Cancelled };
 	}
 
-	const remote = offline
-		? undefined
-		: await createRepositoryOnGitHub(
-				display,
-				{ repository, ...baseOptions.completed },
-				system.fetchers.octokit,
-				system.runner,
-				template,
-			);
+	const remote =
+		offline || !system.fetchers.octokit
+			? undefined
+			: await createRepositoryOnGitHub(
+					display,
+					{ repository, ...baseOptions.completed },
+					system.fetchers.octokit,
+					system.runner,
+					template,
+				);
 
 	if (remote instanceof Error) {
 		logRerunSuggestion(argv, baseOptions.prompted);
diff --git a/packages/bingo/src/runners/applyRequestsToSystem.ts b/packages/bingo/src/runners/applyRequestsToSystem.ts
index dfbc957..e47efe9 100644
--- a/packages/bingo/src/runners/applyRequestsToSystem.ts
+++ b/packages/bingo/src/runners/applyRequestsToSystem.ts
@@ -9,12 +9,17 @@ export async function applyRequestsToSystem(
 ) {
 	await Promise.all(
 		requests.map(async (request) => {
-			const { id, send } = getRequestSender(request);
+			const sender = getRequestSender(system.fetchers, request);
+			if (!sender) {
+				return;
+			}
+
+			const { id, send } = sender;
 
 			system.display.item("request", id, { start: Date.now() });
 
 			try {
-				await send(system.fetchers);
+				await send();
 			} catch (error) {
 				system.display.item("request", id, { error });
 			}
diff --git a/packages/bingo/src/runners/getRequestSender.ts b/packages/bingo/src/runners/getRequestSender.ts
index 335667a..5d48f98 100644
--- a/packages/bingo/src/runners/getRequestSender.ts
+++ b/packages/bingo/src/runners/getRequestSender.ts
@@ -3,30 +3,37 @@ import { SystemFetchers } from "bingo-systems";
 
 export interface RequestSender {
 	id: string;
-	send: (fetchers: SystemFetchers) => Promise<void>;
+	send: () => Promise<void>;
 }
 
-export function getRequestSender(request: CreatedRequest): RequestSender {
+export function getRequestSender(
+	fetchers: SystemFetchers,
+	request: CreatedRequest,
+): RequestSender | undefined {
 	switch (request.type) {
 		case "fetch":
 			return {
 				id: request.id ?? request.url,
-				send: async (fetchers: SystemFetchers) => {
+				send: async () => {
 					await fetchers.fetch(request.url, request.init);
 				},
 			};
 
-		case "octokit":
-			return {
-				id: request.id ?? request.endpoint,
-				send: async (fetchers: SystemFetchers) => {
-					await fetchers.octokit.request(
-						request.endpoint,
-						// TODO: I have no idea how to get this to type-check without the any.
-						// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
-						request.parameters as any,
-					);
-				},
-			};
+		case "octokit": {
+			const { octokit } = fetchers;
+			return (
+				octokit && {
+					id: request.id ?? request.endpoint,
+					send: async () => {
+						await octokit.request(
+							request.endpoint,
+							// TODO: I have no idea how to get this to type-check without the any.
+							// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
+							request.parameters as any,
+						);
+					},
+				}
+			);
+		}
 	}
 }
diff --git a/packages/site/src/content/docs/build/apis/run-creation.mdx b/packages/site/src/content/docs/build/apis/run-creation.mdx
index b273bca..5eca321 100644
--- a/packages/site/src/content/docs/build/apis/run-creation.mdx
+++ b/packages/site/src/content/docs/build/apis/run-creation.mdx
@@ -66,15 +66,22 @@ await runCration(
 
 File, network, and shell wrapper APIs.
 
-By default, these are provided by [`bingo-systems`](/build/packages/bingo-systems).
+`system` may contain the following properties, which have the following defaults:
+
+- `fetchers`: [`createSystemFetchers`](/build/packages/bingo-systems#fetchers)
+- `fs`: [`createWritingFileSystem`](/build/packages/bingo-systems#files)
+- `runner`: [`createSystemRunner`](/build/packages/bingo-systems#runner)
+- `take`: a function that passes those properties to the Input
+
 They can be swapped out if you want to change or otherwise intercept system calls.
 
 For example, this adds a log to every shell command:
 
 ```ts
-import { createSystemContext } from "bingo-systems";
+import { runCreation } from "bingo";
+import { createSystemRunner } from "bingo-systems";
 
-const defaultSystem = createSystemContext();
+const defaultRunner = createSystemRunner();
 
 await runCreation(
 	{
@@ -82,10 +89,9 @@ await runCreation(
 	},
 	{
 		system: {
-			...createSystemContext(),
 			runner: async (...args) => {
 				console.log("Running:", args);
-				return await defaultSystem.runner(args);
+				return await defaultRunner(args);
 			},
 		},
 	},
diff --git a/packages/site/src/content/docs/build/packages/bingo-systems.mdx b/packages/site/src/content/docs/build/packages/bingo-systems.mdx
index 277362f..3355231 100644
--- a/packages/site/src/content/docs/build/packages/bingo-systems.mdx
+++ b/packages/site/src/content/docs/build/packages/bingo-systems.mdx
@@ -41,12 +41,12 @@ import { Octokit } from "octokit";
 
 export interface SystemFetchers {
 	fetch: typeof fetch;
-	octokit: Octokit;
+	octokit: Octokit | undefined;
 }
 ```
 
-The `octokit` property is also available as a convenience for sending GitHub API requests.
-This is typically used to populate repository settings via the API.
+The `octokit` property is optionally available for sending GitHub API requests.
+This is typically used to populate repository settings via GitHub's API.
 
 ### `createSystemFetchers`
 
@@ -62,7 +62,7 @@ await fetchers.fetch("...");
 
 Takes in an optional object parameter with up to two properties:
 
-- `auth` (`string`): a GitHub auth token to pass to the `octokit`
+- `auth` (`string`): a GitHub auth token create the `octokit` property with
 - `fetch`: a function to use in place of the global [`fetch`](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API)
   - This is used both for the `fetch` property and in the `octokit`
 


Test patch:
diff --git a/packages/bingo-systems/src/fetchers/createSystemFetchers.test.ts b/packages/bingo-systems/src/fetchers/createSystemFetchers.test.ts
new file mode 100644
index 0000000..1179686
--- /dev/null
+++ b/packages/bingo-systems/src/fetchers/createSystemFetchers.test.ts
@@ -0,0 +1,42 @@
+import { Octokit } from "octokit";
+import { describe, expect, it, vi } from "vitest";
+
+import { createSystemFetchers } from "./createSystemFetchers.js";
+
+describe("createSystemFetchers", () => {
+	it("returns an object with only fetch when no auth is provided", () => {
+		const fetchers = createSystemFetchers({});
+
+		expect(fetchers).toEqual({
+			fetch: globalThis.fetch,
+			octokit: undefined,
+		});
+	});
+
+	it("returns an object with fetch and octokit when auth is provided", () => {
+		const fetchers = createSystemFetchers({ auth: "abc123" });
+
+		expect(fetchers).toEqual({
+			fetch: globalThis.fetch,
+			octokit: expect.any(Octokit),
+		});
+	});
+
+	it("returns an object with the custom fetch when fetch is provided", () => {
+		const fetch = vi.fn();
+		const fetchers = createSystemFetchers({ fetch });
+
+		expect(fetchers).toEqual({ fetch });
+	});
+
+	it("returns an object with the custom fetch and octokit when auth and fetch are provided", () => {
+		const auth = "abc123";
+		const fetch = vi.fn();
+		const fetchers = createSystemFetchers({ auth, fetch });
+
+		expect(fetchers).toEqual({
+			fetch,
+			octokit: expect.any(Octokit),
+		});
+	});
+});
diff --git a/packages/bingo/src/runners/applyRequestsToSystem.test.ts b/packages/bingo/src/runners/applyRequestsToSystem.test.ts
index d114dc5..9e8ce9f 100644
--- a/packages/bingo/src/runners/applyRequestsToSystem.test.ts
+++ b/packages/bingo/src/runners/applyRequestsToSystem.test.ts
@@ -29,6 +29,24 @@ function createStubSystem() {
 }
 
 describe("applyRequestsToSystem", () => {
+	it("does not do anything when getRequestSender returns undefined", async () => {
+		mockGetRequestSender.mockReturnValueOnce(undefined);
+
+		const act = async () => {
+			await applyRequestsToSystem(
+				[
+					{
+						type: "fetch",
+						url: "https://example.com",
+					},
+				],
+				createStubSystem(),
+			);
+		};
+
+		await expect(act()).resolves.toBeUndefined();
+	});
+
 	it("does not display an error when a request  does not reject", async () => {
 		const id = "abc";
 		const send = vi.fn();
diff --git a/packages/bingo/src/runners/getRequestSender.test.ts b/packages/bingo/src/runners/getRequestSender.test.ts
index 455ac91..ba9d514 100644
--- a/packages/bingo/src/runners/getRequestSender.test.ts
+++ b/packages/bingo/src/runners/getRequestSender.test.ts
@@ -21,7 +21,10 @@ describe("getRequestSender", () => {
 			url: "https://create.bingo",
 		};
 
-		const actual = getRequestSender(request);
+		const actual = getRequestSender(
+			createMockSystemFetchers() as unknown as SystemFetchers,
+			request,
+		);
 
 		expect(actual).toEqual({
 			id: request.url,
@@ -36,7 +39,10 @@ describe("getRequestSender", () => {
 			url: "https://create.bingo",
 		};
 
-		const actual = getRequestSender(request);
+		const actual = getRequestSender(
+			createMockSystemFetchers() as unknown as SystemFetchers,
+			request,
+		);
 
 		expect(actual).toEqual({
 			id: request.id,
@@ -44,7 +50,7 @@ describe("getRequestSender", () => {
 		});
 	});
 
-	it("sends a request with fetchers.request sending a fetch request", async () => {
+	it("sends a request with fetchers.request when sending a fetch request", async () => {
 		const fetchers = createMockSystemFetchers();
 
 		const request: CreatedFetchRequest = {
@@ -53,12 +59,31 @@ describe("getRequestSender", () => {
 			url: "https://create.bingo",
 		};
 
-		await getRequestSender(request).send(fetchers as unknown as SystemFetchers);
+		// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+		await getRequestSender(
+			fetchers as unknown as SystemFetchers,
+			request,
+		)!.send();
 
 		expect(fetchers.fetch).toHaveBeenCalledWith(request.url, request.init);
 		expect(fetchers.octokit.request).not.toHaveBeenCalled();
 	});
 
+	it("returns undefined when given an Octokit request and fetchers.octokit is defined", () => {
+		const request: CreatedOctokitRequest = {
+			endpoint: "POST /repos/{owner}/{repo}/labels",
+			parameters: { name: "...", owner, repo },
+			type: "octokit",
+		};
+
+		const actual = getRequestSender(
+			{ fetch: vi.fn(), octokit: undefined } as SystemFetchers,
+			request,
+		);
+
+		expect(actual).toBeUndefined();
+	});
+
 	it("returns a sender with url for id when given an Octokit request without id", () => {
 		const request: CreatedOctokitRequest = {
 			endpoint: "POST /repos/{owner}/{repo}/labels",
@@ -66,7 +91,10 @@ describe("getRequestSender", () => {
 			type: "octokit",
 		};
 
-		const actual = getRequestSender(request);
+		const actual = getRequestSender(
+			createMockSystemFetchers() as unknown as SystemFetchers,
+			request,
+		);
 
 		expect(actual).toEqual({
 			id: request.endpoint,
@@ -82,7 +110,10 @@ describe("getRequestSender", () => {
 			type: "octokit",
 		};
 
-		const actual = getRequestSender(request);
+		const actual = getRequestSender(
+			createMockSystemFetchers() as unknown as SystemFetchers,
+			request,
+		);
 
 		expect(actual).toEqual({
 			id: request.id,
@@ -90,7 +121,7 @@ describe("getRequestSender", () => {
 		});
 	});
 
-	it("sends a request with fetchers.octokit.request sending an Octokit request", async () => {
+	it("sends a request with the Octokit when sending an Octokit request and fetchers.octokit is defined", async () => {
 		const fetchers = createMockSystemFetchers();
 
 		const request: CreatedOctokitRequest = {
@@ -99,7 +130,11 @@ describe("getRequestSender", () => {
 			type: "octokit",
 		};
 
-		await getRequestSender(request).send(fetchers as unknown as SystemFetchers);
+		// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+		await getRequestSender(
+			fetchers as unknown as SystemFetchers,
+			request,
+		)!.send();
 
 		expect(fetchers.fetch).not.toHaveBeenCalled();
 		expect(fetchers.octokit.request).toHaveBeenCalledWith(