Skip to content

Commit 0307eba

Browse files
feat: automate repository creation for --mode create (#690)
## PR Checklist - [x] Addresses an existing open issue: fixes #688 - [x] That issue was marked as [`status: accepting prs`](https://github.com/JoshuaKGoldberg/template-typescript-node-package/issues?q=is%3Aopen+is%3Aissue+label%3A%22status%3A+accepting+prs%22) - [x] Steps in [CONTRIBUTING.md](https://github.com/JoshuaKGoldberg/template-typescript-node-package/blob/main/.github/CONTRIBUTING.md) were taken ## Overview Streamlines the heck out of the `--mode create` (`npx template-typescript-node-package`) experience: * Instead of telling you to run some initialize commands, it offers to run them for you * Similarly, after creating a new repository, the creation script will offer to update the repo using GitHub's APIs for you * Adds a `--create-repository` flag to automatically do those things
1 parent 0445479 commit 0307eba

15 files changed

+334
-135
lines changed

Diff for: docs/InitializationFromTemplate.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ pnpm run initialize
2121
```
2222

2323
The initialization script will interactively prompt for values to be used in creating the new repository.
24-
Each may provided as a string CLI flag as well:
24+
Each may provided as a CLI flag as well:
2525

2626
- `--description`: Sentence case description of the repository (e.g. `A quickstart-friendly TypeScript package with lots of great repository tooling. ✨`)
2727
- `--owner`: GitHub organization or user the repository is underneath (e.g. `JoshuaKGoldberg`)

Diff for: docs/InitializationFromTerminal.md

+11-13
Original file line numberDiff line numberDiff line change
@@ -13,32 +13,30 @@ Pass `--mode create` to tell it to create a new repository in a child directory:
1313
npx template-typescript-node-package --mode create
1414
```
1515

16-
Upon completion, the prompt will suggest commands to get started working in the repository and create a corresponding GitHub repository.
17-
Those commands will roughly run the [The Initialization Script](./InitializationFromTemplate.md#the-initialization-script), but with `npm template-typescript-node-package --mode initialize` instead of `pnpm run initialize`:
16+
Then, go through the following two steps to set up required repository tooling on GitHub:
1817

19-
```shell
20-
cd repository-name
21-
gh repo create repository-name --public --source=. --remote=origin
22-
npx template-typescript-node-package --mode initialize
23-
git add -A
24-
git commit -m "chore: initial commit ✨"
25-
git push -u origin main
26-
```
18+
1. Create two tokens in [repository secrets](https://docs.github.com/en/actions/security-guides/encrypted-secrets):
19+
- `ACCESS_TOKEN`: A [GitHub PAT](https://github.com/settings/tokens/new) with _repo_ and _workflow_ permissions
20+
- `NPM_TOKEN`: An [npm access token](https://docs.npmjs.com/creating-and-viewing-access-tokens/) with _Automation_ permissions
21+
2. Install the [Codecov GitHub App](https://github.com/marketplace/codecov) and [Renovate GitHub App](https://github.com/marketplace/renovate)
22+
23+
At this point, your new repository should be ready for development! 🥳
2724

2825
## The Creation Script
2926

3027
The creation script will interactively prompt for values to be used in creating the new repository.
31-
Each may provided as a string CLI flag as well:
28+
Each may provided as a CLI flag as well:
3229

30+
- `--create-repository`: Whether to automatically create a repository on github.com with the given repository name under the owner
3331
- `--description`: Sentence case description of the repository (e.g. `A quickstart-friendly TypeScript package with lots of great repository tooling. ✨`)
3432
- `--owner`: GitHub organization or user the repository is underneath (e.g. `JoshuaKGoldberg`)
3533
- `--repository`: The kebab-case name of the repository (e.g. `template-typescript-node-package`)
3634
- `--title`: Title Case title for the repository to be used in documentation (e.g. `Template TypeScript Node Package`)
3735

38-
For example, pre-populating all values:
36+
For example, pre-populating all values and creating a new repository:
3937

4038
```shell
41-
npx template-typescript-node-package --mode create --repository "testing-repository" --title "Testing Title" --owner "TestingOwner" --description "Test Description"
39+
npx template-typescript-node-package --create-repository --mode create --repository "testing-repository" --title "Testing Title" --owner "TestingOwner" --description "Test Description"
4240
```
4341

4442
Then, go through the following two steps to set up required repository tooling on GitHub:

Diff for: src/bin/index.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,9 @@ export async function bin(args: string[]) {
1313

1414
prompts.intro(
1515
[
16-
chalk.greenBright`Welcome to the`,
17-
chalk.bgGreenBright.black`template-typescript-node-package`,
18-
chalk.greenBright(`package! 🎉`),
16+
chalk.greenBright(`Welcome to`),
17+
chalk.bgGreenBright.black(`template-typescript-node-package`),
18+
chalk.greenBright(`! 🎉`),
1919
].join(" "),
2020
);
2121

Diff for: src/create/createWithValues.ts

+31
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,14 @@
1+
import * as prompts from "@clack/prompts";
2+
import { $ } from "execa";
3+
14
import { runOrSkip } from "../shared/cli/runOrSkip.js";
25
import { withSpinner } from "../shared/cli/spinners.js";
6+
import { doesRepositoryExist } from "../shared/doesRepositoryExist.js";
37
import { HelpersAndValues } from "../shared/inputs.js";
48
import { finalizeDependencies } from "../steps/finalizeDependencies.js";
9+
import { initializeBranchProtectionSettings } from "../steps/initializeBranchProtectionSettings.js";
10+
import { initializeRepositorySettings } from "../steps/initializeRepositorySettings.js";
11+
import { initializeRepositoryLabels } from "../steps/labels/initializeRepositoryLabels.js";
512
import { runCommands } from "../steps/runCommands.js";
613
import { writeReadme } from "../steps/writeReadme.js";
714
import { writeStructure } from "../steps/writing/writeStructure.js";
@@ -30,4 +37,28 @@ export async function createWithValues(input: HelpersAndValues) {
3037
"pnpm format --write",
3138
"pnpm lint --fix",
3239
]);
40+
41+
const sendToGitHub =
42+
input.octokit &&
43+
(await doesRepositoryExist(input.octokit, input.values)) &&
44+
(input.values.createRepository ??
45+
(await prompts.confirm({
46+
message:
47+
"Would you like to push the template's tooling up to the repository on GitHub?",
48+
})) === true);
49+
50+
await runOrSkip("Initializing API metadata", !sendToGitHub, async () => {
51+
await $`git remote add origin https://github.com/${input.values.owner}/${input.values.repository}`;
52+
await $`git add -A`;
53+
await $`git commit --message ${"chore: initialized repo ✨"}`;
54+
await $`git push -u origin main --force`;
55+
56+
/* eslint-disable @typescript-eslint/no-non-null-assertion */
57+
await initializeBranchProtectionSettings(input.octokit!, input.values);
58+
await initializeRepositoryLabels();
59+
await initializeRepositorySettings(input.octokit!, input.values);
60+
/* eslint-enable @typescript-eslint/no-non-null-assertion */
61+
});
62+
63+
return { sentToGitHub: sendToGitHub };
3364
}

Diff for: src/create/index.ts

+28-19
Original file line numberDiff line numberDiff line change
@@ -34,29 +34,38 @@ export async function create(args: string[]) {
3434

3535
return await runOrRestore({
3636
run: async () => {
37-
await createWithValues({
37+
const { sentToGitHub } = await createWithValues({
3838
...inputs,
3939
values: await augmentValuesWithNpmInfo(inputs.values),
4040
});
4141

42-
outro([
43-
{
44-
label:
45-
"Consider creating a GitHub repository from the new directory:",
46-
lines: [
47-
`cd ${inputs.values.repository}`,
48-
`gh repo create ${inputs.values.repository} --public --source=. --remote=origin`,
49-
`npx template-typescript-node-package --mode initialize`,
50-
`git add -A`,
51-
`git commit -m "chore: initial commit ✨"`,
52-
`git push -u origin main`,
53-
],
54-
},
55-
{
56-
label:
57-
"If you do, be sure to populate its ACCESS_TOKEN and NPM_TOKEN secrets.",
58-
},
59-
]);
42+
outro(
43+
sentToGitHub
44+
? [
45+
{
46+
label:
47+
"Now, all you have to do is populate the repository's ACCESS_TOKEN and NPM_TOKEN secrets.",
48+
},
49+
]
50+
: [
51+
{
52+
label:
53+
"Consider creating a GitHub repository from the new directory:",
54+
lines: [
55+
`cd ${inputs.values.repository}`,
56+
`gh repo create ${inputs.values.repository} --public --source=. --remote=origin`,
57+
`npx template-typescript-node-package --mode initialize`,
58+
`git add -A`,
59+
`git commit -m "chore: initial commit ✨"`,
60+
`git push -u origin main`,
61+
],
62+
},
63+
{
64+
label:
65+
"If you do, be sure to populate its ACCESS_TOKEN and NPM_TOKEN secrets.",
66+
},
67+
],
68+
);
6069
},
6170
skipRestore: inputs.values.skipRestore ?? true,
6271
});

Diff for: src/initialize/initializeWithValues.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -36,9 +36,9 @@ export async function initializeWithValues(input: HelpersAndValues) {
3636

3737
await runOrSkip("Initializing API metadata", !input.octokit, async () => {
3838
/* eslint-disable @typescript-eslint/no-non-null-assertion */
39+
await initializeBranchProtectionSettings(input.octokit!, input.values);
3940
await initializeRepositoryLabels();
4041
await initializeRepositorySettings(input.octokit!, input.values);
41-
await initializeBranchProtectionSettings(input.octokit!, input.values);
4242
/* eslint-enable @typescript-eslint/no-non-null-assertion */
4343
});
4444

Diff for: src/shared/args.ts

+1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
export const allArgOptions = {
22
author: { type: "string" },
3+
"create-repository": { type: "boolean" },
34
description: { type: "string" },
45
email: { type: "string" },
56
funding: { type: "string" },

Diff for: src/shared/augmentValuesWithNpmInfo.ts

+1
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ export async function augmentValuesWithNpmInfo<Values extends InputValues>(
1414
author: await getPrefillOrPromptedValue(
1515
values.author,
1616
"What author (username) will be used for the npm owner?",
17+
values.owner.toLowerCase(),
1718
),
1819
email: await getPrefillOrPromptedValue(
1920
values.email,

Diff for: src/shared/doesRepositoryExist.test.ts

+47
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import { Octokit } from "octokit";
2+
import { SpyInstance, describe, expect, it, vi } from "vitest";
3+
4+
import { doesRepositoryExist } from "./doesRepositoryExist.js";
5+
6+
const createMockOctokit = (
7+
repos: Partial<Record<keyof Octokit["rest"]["repos"], SpyInstance>>,
8+
) =>
9+
({
10+
rest: {
11+
repos,
12+
},
13+
}) as unknown as Octokit;
14+
15+
const owner = "StubOwner";
16+
const repository = "stub-repository";
17+
18+
describe("doesRepositoryExist", () => {
19+
it("returns true when the octokit GET resolves", async () => {
20+
const octokit = createMockOctokit({ get: vi.fn().mockResolvedValue({}) });
21+
22+
const actual = await doesRepositoryExist(octokit, { owner, repository });
23+
24+
expect(actual).toEqual(true);
25+
});
26+
27+
it("returns false when the octokit GET rejects with a 404", async () => {
28+
const octokit = createMockOctokit({
29+
get: vi.fn().mockRejectedValue({ status: 404 }),
30+
});
31+
32+
const actual = await doesRepositoryExist(octokit, { owner, repository });
33+
34+
expect(actual).toEqual(false);
35+
});
36+
37+
it("throws the error when awaiting the octokit GET throws a non-404 error", async () => {
38+
const error = new Error("Oh no!");
39+
const octokit = createMockOctokit({
40+
get: vi.fn().mockRejectedValue(error),
41+
});
42+
43+
await expect(
44+
async () => await doesRepositoryExist(octokit, { owner, repository }),
45+
).rejects.toEqual(error);
46+
});
47+
});

Diff for: src/shared/doesRepositoryExist.ts

+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { Octokit, RequestError } from "octokit";
2+
3+
export interface DoesRepositoryExistOptions {
4+
owner: string;
5+
repository: string;
6+
}
7+
8+
export async function doesRepositoryExist(
9+
octokit: Octokit,
10+
options: DoesRepositoryExistOptions,
11+
) {
12+
// Because the Octokit SDK throws on 404s (😡),
13+
// we try/catch to check whether the repo exists.
14+
try {
15+
await octokit.rest.repos.get({
16+
owner: options.owner,
17+
repo: options.repository,
18+
});
19+
return true;
20+
} catch (error) {
21+
if ((error as RequestError).status !== 404) {
22+
throw error;
23+
}
24+
25+
return false;
26+
}
27+
}

0 commit comments

Comments
 (0)