Skip to content

Commit d112a31

Browse files
feat: allow specific GitHub and/or npm emails (#858)
## PR Checklist - [x] Addresses an existing open issue: fixes #542 - [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 Builds off the ideas in #851 by adding optional `--email-github` and `--email-npm` overrides. Adds a string complaint if an improper combination is specified: one of those two without the other or `email`, or both of those two along with `email`. At this point `email` is now a required option. Adjusts the sorting of rerun suggestions a bit to make sure emails are sorted inline and `--base` comes immediately after `--mode`. Also fills in some unit test coverage in the area.
1 parent 5c0cf29 commit d112a31

30 files changed

+501
-106
lines changed

.github/CODE_OF_CONDUCT.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ representative at an online or offline event.
6060

6161
Instances of abusive, harassing, or otherwise unacceptable behavior may be
6262
reported to the community leaders responsible for enforcement at
63-
npm@joshuakgoldberg.com.
63+
github@joshuakgoldberg.com.
6464
All complaints will be reviewed and investigated promptly and fairly.
6565

6666
All community leaders are obligated to respect the privacy and security of the

.github/SECURITY.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,6 @@ We take all security vulnerabilities seriously.
44
If you have a vulnerability or other security issues to disclose:
55

66
- Thank you very much, please do!
7-
- Please send them to us by emailing `npm@joshuakgoldberg.com`
7+
- Please send them to us by emailing `github@joshuakgoldberg.com`
88

99
We appreciate your efforts and responsible disclosure and will make every effort to acknowledge your contributions.

docs/Options.md

+1
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ The setup scripts also allow for optional overrides of the following inputs whos
5151

5252
- `--author` _(`string`)_: Username on npm to publish packages under (by default, an existing npm author, or the currently logged in npm user, or `owner.toLowerCase()`)
5353
- `--email` _(`string`)_: Email address to be listed as the point of contact in docs and packages (e.g. `[email protected]`)
54+
- Optionally, `--email-github` _(`string`)_ and/or `--email-npm` _(`string`)_ may be provided to use different emails in `.md` files and `package.json`, respectively
5455
- `--funding` _(`string`)_: GitHub organization or username to mention in `funding.yml` (by default, `owner`)
5556
- `--logo` _(`string`)_: Local image file in the repository to display near the top of the README.md as a logo
5657
- `--logo-alt` _(`string`)_: If `--logo` is provided or detected from an existing README.md, alt text that describes the image will be prompted for if not provided

script/migrate-test-e2e.js

+3-1
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,14 @@ import { $, execaCommand } from "execa";
44
import packageData from "../package.json" assert { type: "json" };
55

66
const { description, name: repository } = packageData;
7+
const emailGithub = "[email protected]";
8+
const emailNpm = "[email protected]";
79
const owner = "JoshuaKGoldberg";
810
const title = "Create TypeScript App";
911

1012
await $({
1113
stdio: "inherit",
12-
})`c8 -o ./coverage-migrate -r html -r lcov --src src node ./bin/index.js --base everything --mode migrate --description ${description} --owner ${owner} --title ${title} --repository ${repository} --exclude-contributors --skip-github-api --skip-install`;
14+
})`c8 -o ./coverage-migrate -r html -r lcov --src src node ./bin/index.js --base everything --mode migrate --description ${description} --email-github ${emailGithub} --email-npm ${emailNpm} --owner ${owner} --title ${title} --repository ${repository} --exclude-contributors --skip-github-api --skip-install`;
1315

1416
const { stdout: gitStatus } = await $`git status`;
1517
console.log(`Stdout from running \`git status\`:\n${gitStatus}`);

src/bin/index.test.ts

+25-2
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,30 @@ describe("bin", () => {
120120
expect(result).toEqual(code);
121121
});
122122

123-
it("returns the cancel result containing zod error of the corresponding runner and output plus cancel logs when promptForMode returns a mode that cancels", async () => {
123+
it("returns the cancel result containing a zod error of the corresponding runner and output plus cancel logs when promptForMode returns a mode that cancels with a string error", async () => {
124+
const mode = "initialize";
125+
const args = ["--email", "abc123"];
126+
const code = 2;
127+
const error = "Oh no!";
128+
129+
mockPromptForMode.mockResolvedValue(mode);
130+
mockInitialize.mockResolvedValue({
131+
code: 2,
132+
error,
133+
options: {},
134+
});
135+
136+
const result = await bin(args);
137+
138+
expect(mockInitialize).toHaveBeenCalledWith(args);
139+
expect(mockLogLine).toHaveBeenCalledWith(chalk.red(error));
140+
expect(mockCancel).toHaveBeenCalledWith(
141+
`Operation cancelled. Exiting - maybe another time? 👋`,
142+
);
143+
expect(result).toEqual(code);
144+
});
145+
146+
it("returns the cancel result containing a zod error of the corresponding runner and output plus cancel logs when promptForMode returns a mode that cancels with a zod error", async () => {
124147
const mode = "initialize";
125148
const args = ["--email", "abc123"];
126149
const code = 2;
@@ -132,8 +155,8 @@ describe("bin", () => {
132155
mockPromptForMode.mockResolvedValue(mode);
133156
mockInitialize.mockResolvedValue({
134157
code: 2,
158+
error: (validationResult as z.SafeParseError<{ email: string }>).error,
135159
options: {},
136-
zodError: (validationResult as z.SafeParseError<{ email: string }>).error,
137160
});
138161

139162
const result = await bin(args);

src/bin/index.ts

+5-4
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ export async function bin(args: string[]) {
5252
}
5353

5454
const runners = { create, initialize, migrate };
55-
const { code, options, zodError } = await runners[mode](args);
55+
const { code, error, options } = await runners[mode](args);
5656

5757
prompts.log.info(
5858
[
@@ -64,9 +64,10 @@ export async function bin(args: string[]) {
6464
if (code) {
6565
logLine();
6666

67-
if (zodError) {
68-
const validationError = fromZodError(zodError);
69-
logLine(chalk.red(validationError));
67+
if (error) {
68+
logLine(
69+
chalk.red(typeof error === "string" ? error : fromZodError(error)),
70+
);
7071
logLine();
7172
}
7273

src/bin/mode.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@ import { Options } from "../shared/types.js";
88

99
export interface ModeResult {
1010
code: StatusCode;
11+
error?: string | z.ZodError<object>;
1112
options: Partial<Options>;
12-
zodError?: z.ZodError<object>;
1313
}
1414

1515
export type ModeRunner = (args: string[]) => Promise<ModeResult>;

src/create/createRerunSuggestion.test.ts

+7-4
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,10 @@ const options = {
88
base: "everything",
99
createRepository: true,
1010
description: "Test description.",
11-
11+
email: {
12+
github: "[email protected]",
13+
14+
},
1215
excludeCompliance: true,
1316
excludeContributors: true,
1417
excludeLintJson: true,
@@ -39,7 +42,7 @@ describe("createRerunSuggestion", () => {
3942
const actual = createRerunSuggestion("initialize", options);
4043

4144
expect(actual).toMatchInlineSnapshot(
42-
'"npx create-typescript-app --mode initialize --author TestAuthor --base everything --create-repository true --description \\"Test description.\\" --email test@test.com --exclude-compliance true --exclude-contributors true --exclude-lint-json true --exclude-lint-knip true --exclude-lint-package-json true --exclude-lint-perfectionist true --owner TestOwner --repository test-repository --skip-github-api true --skip-install true --skip-removal true --title \\"Test Title\\""',
45+
'"npx create-typescript-app --mode initialize --base everything --author TestAuthor --create-repository true --description \\"Test description.\\" --email-github [email protected] --email-npm npm@email.com --exclude-compliance true --exclude-contributors true --exclude-lint-json true --exclude-lint-knip true --exclude-lint-package-json true --exclude-lint-perfectionist true --owner TestOwner --repository test-repository --skip-github-api true --skip-install true --skip-removal true --title \\"Test Title\\""',
4346
);
4447
});
4548

@@ -53,7 +56,7 @@ describe("createRerunSuggestion", () => {
5356
});
5457

5558
expect(actual).toMatchInlineSnapshot(
56-
'"npx create-typescript-app --mode initialize --author TestAuthor --base everything --create-repository true --description \\"Test description.\\" --email test@test.com --exclude-compliance true --exclude-contributors true --exclude-lint-json true --exclude-lint-knip true --exclude-lint-package-json true --exclude-lint-perfectionist true --logo test/src.png --owner TestOwner --repository test-repository --skip-github-api true --skip-install true --skip-removal true --title \\"Test Title\\" --logo-alt \\"Test alt.\\""',
59+
'"npx create-typescript-app --mode initialize --base everything --author TestAuthor --create-repository true --description \\"Test description.\\" --email-github github@email.com --email-npm [email protected] --exclude-compliance true --exclude-contributors true --exclude-lint-json true --exclude-lint-knip true --exclude-lint-package-json true --exclude-lint-perfectionist true --logo test/src.png --logo-alt \\"Test alt.\\" --owner TestOwner --repository test-repository --skip-github-api true --skip-install true --skip-removal true --title \\"Test Title\\""',
5760
);
5861
});
5962

@@ -66,7 +69,7 @@ describe("createRerunSuggestion", () => {
6669
});
6770

6871
expect(actual).toMatchInlineSnapshot(
69-
'"npx create-typescript-app --mode initialize --author TestAuthor --base everything --create-repository true --description \\"Test description.\\" --email test@test.com --exclude-compliance true --exclude-contributors true --exclude-lint-json true --exclude-lint-knip true --exclude-lint-md true --exclude-lint-package-json true --exclude-lint-perfectionist true --exclude-lint-spelling true --owner TestOwner --repository test-repository --skip-github-api true --skip-install true --skip-removal true --title \\"Test Title\\""',
72+
'"npx create-typescript-app --mode initialize --base everything --author TestAuthor --create-repository true --description \\"Test description.\\" --email-github [email protected] --email-npm npm@email.com --exclude-compliance true --exclude-contributors true --exclude-lint-json true --exclude-lint-knip true --exclude-lint-md true --exclude-lint-package-json true --exclude-lint-perfectionist true --exclude-lint-spelling true --owner TestOwner --repository test-repository --skip-github-api true --skip-install true --skip-removal true --title \\"Test Title\\""',
7073
);
7174
});
7275
});

src/create/createRerunSuggestion.ts

+10
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,13 @@ export function createRerunSuggestion(
1414
): string {
1515
const optionsNormalized = {
1616
...options,
17+
...(options.email
18+
? {
19+
email: undefined,
20+
emailGitHub: options.email.github,
21+
emailNpm: options.email.npm,
22+
}
23+
: { email: undefined }),
1724
...(options.logo
1825
? {
1926
logo: options.logo.src,
@@ -23,6 +30,9 @@ export function createRerunSuggestion(
2330
};
2431

2532
const args = Object.entries(optionsNormalized)
33+
.sort(([a], [b]) =>
34+
a === "base" ? -1 : b === "base" ? 1 : a.localeCompare(b),
35+
)
2636
.filter(([, value]) => !!value)
2737
.map(([key, value]) => {
2838
const valueStringified = `${value}`;

src/create/index.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,8 @@ export async function create(args: string[]): Promise<ModeResult> {
1515
if (inputs.cancelled) {
1616
return {
1717
code: StatusCodes.Cancelled,
18+
error: inputs.error,
1819
options: inputs.options,
19-
zodError: inputs.zodError,
2020
};
2121
}
2222

src/initialize/index.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@ export const initialize: ModeRunner = async (args) => {
1111
if (inputs.cancelled) {
1212
return {
1313
code: StatusCodes.Cancelled,
14+
error: inputs.error,
1415
options: inputs.options,
15-
zodError: inputs.zodError,
1616
};
1717
}
1818

src/migrate/index.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@ export const migrate: ModeRunner = async (args) => {
1111
if (inputs.cancelled) {
1212
return {
1313
code: StatusCodes.Cancelled,
14+
error: inputs.error,
1415
options: inputs.options,
15-
zodError: inputs.zodError,
1616
};
1717
}
1818

src/shared/options/args.ts

+2
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ export const allArgOptions = {
44
"create-repository": { type: "boolean" },
55
description: { type: "string" },
66
email: { type: "string" },
7+
"email-github": { type: "string" },
8+
"email-npm": { type: "string" },
79
"exclude-compliance": { type: "boolean" },
810
"exclude-contributors": { type: "boolean" },
911
"exclude-lint-json": { type: "boolean" },

src/shared/options/augmentOptionsWithExcludes.test.ts

+4-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,10 @@ const optionsBase = {
88
base: undefined,
99
createRepository: undefined,
1010
description: "",
11-
email: undefined,
11+
email: {
12+
github: "[email protected]",
13+
14+
},
1215
excludeCompliance: undefined,
1316
excludeContributors: undefined,
1417
excludeLintJson: undefined,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { describe, expect, it } from "vitest";
2+
3+
import { detectEmailRedundancy } from "./detectEmailRedundancy.js";
4+
5+
describe("detectEmailRedundancy", () => {
6+
it("returns undefined when only email is specified", () => {
7+
expect(detectEmailRedundancy({ email: "[email protected]" })).toBeUndefined();
8+
});
9+
10+
it("returns undefined when email-github and email-npm are specified while email is not", () => {
11+
expect(
12+
detectEmailRedundancy({
13+
"email-github": "[email protected]",
14+
"email-npm": "[email protected]",
15+
}),
16+
).toBeUndefined();
17+
});
18+
19+
it("returns a complaint when email-github is specified while email and email-npm are not", () => {
20+
expect(
21+
detectEmailRedundancy({
22+
"email-github": "[email protected]",
23+
}),
24+
).toBe(
25+
"If --email-github is specified, either --email or --email-npm should be.",
26+
);
27+
});
28+
29+
it("returns a complaint when email-npm is specified while email and email-github are not", () => {
30+
expect(
31+
detectEmailRedundancy({
32+
"email-npm": "[email protected]",
33+
}),
34+
).toBe(
35+
"If --email-npm is specified, either --email or --email-github should be.",
36+
);
37+
});
38+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
export interface EmailValues {
2+
email?: boolean | string;
3+
"email-github"?: boolean | string;
4+
"email-npm"?: boolean | string;
5+
}
6+
7+
export function detectEmailRedundancy(values: EmailValues) {
8+
if (values.email) {
9+
return values["email-github"] && values["email-npm"]
10+
? "--email should not be specified if both --email-github and --email-npm are specified."
11+
: undefined;
12+
}
13+
14+
if (values["email-github"] && !values["email-npm"]) {
15+
return "If --email-github is specified, either --email or --email-npm should be.";
16+
}
17+
18+
if (values["email-npm"] && !values["email-github"]) {
19+
return "If --email-npm is specified, either --email or --email-github should be.";
20+
}
21+
22+
return undefined;
23+
}

src/shared/options/optionsSchema.ts

+47
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import { z } from "zod";
2+
3+
export const optionsSchemaShape = {
4+
author: z.string().optional(),
5+
base: z
6+
.union([
7+
z.literal("common"),
8+
z.literal("everything"),
9+
z.literal("minimum"),
10+
z.literal("prompt"),
11+
])
12+
.optional(),
13+
createRepository: z.boolean().optional(),
14+
description: z.string().optional(),
15+
email: z
16+
.object({
17+
github: z.string().email(),
18+
npm: z.string().email(),
19+
})
20+
.optional(),
21+
excludeCompliance: z.boolean().optional(),
22+
excludeContributors: z.boolean().optional(),
23+
excludeLintJson: z.boolean().optional(),
24+
excludeLintKnip: z.boolean().optional(),
25+
excludeLintMd: z.boolean().optional(),
26+
excludeLintPackageJson: z.boolean().optional(),
27+
excludeLintPackages: z.boolean().optional(),
28+
excludeLintPerfectionist: z.boolean().optional(),
29+
excludeLintSpelling: z.boolean().optional(),
30+
excludeLintYml: z.boolean().optional(),
31+
excludeReleases: z.boolean().optional(),
32+
excludeRenovate: z.boolean().optional(),
33+
excludeTests: z.boolean().optional(),
34+
funding: z.string().optional(),
35+
logo: z.string().optional(),
36+
logoAlt: z.string().optional(),
37+
owner: z.string().optional(),
38+
repository: z.string().optional(),
39+
skipGitHubApi: z.boolean().optional(),
40+
skipInstall: z.boolean().optional(),
41+
skipRemoval: z.boolean().optional(),
42+
skipRestore: z.boolean().optional(),
43+
skipUninstall: z.boolean().optional(),
44+
title: z.string().optional(),
45+
};
46+
47+
export const optionsSchema = z.object(optionsSchemaShape);

0 commit comments

Comments
 (0)