diff --git a/README.md b/README.md
index 46f67b8..56321b0 100644
--- a/README.md
+++ b/README.md
@@ -10,25 +10,38 @@
# WHAT IS IT?
-Codefather protects your codebase by controlling who can change what. Set authorization levels, lock down files, and enforce your rules—offline via CLI or online with GitHub Actions.
+**Codefather protects your codebase by controlling who can change what. Set authorization levels, lock down files, and enforce your rules—offline via CLI or online with GitHub Actions.**
ℹ️ The documentation is also available on our [website](https://donedeal0.gitbook.io/codefather/)!
-## FEATURES
+## CODEOWNERS COMPARISON
+
+**Codefather** can serve as a drop-in replacement for GitHub’s CODEOWNERS—or play alongside it like a trusted consigliere.
+
+GitHub’s [CODEOWNERS](https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners) lets you define file owners in your codebase and automatically assign them as reviewers. No pull request can be merged until an appropriate codeowner has approved it.
+
+**Codefather** offers more flexibility in assigning codeowners: support for various roles (teams, leads, developers), complex file-match rules, local execution, commit protection, and more. It can prevent unauthorized changes, warn developers, list prohibited files with error levels and contact points, block sensitive merges via GitHub Actions, and auto-assign reviewers when needed.
+
+**Codefather** is designed to offer a delightful developer experience—a single config file for both CLI and GitHub Action usage, efficient commands to protect your codebase, automatic translation of CODEOWNERS into Codefather config, and over 100 personalized reactions to your commits.
+
+**Whether you're enforcing strict governance or just want the Don watching over your commits, Codefather brings clarity, control, and charisma to your workflow.**
| FEATURE | CODEFATHER | GITHUB CODEOWNERS |
|--|--|--|
|Files and folders protection | ✅ | ✅ |
|Github Action | ✅ | ✅ |
|Auto-assign reviewers | ✅ | ✅ |
-|Supports teams | ✅ | ✅ |
+|Teams support | ✅ | ✅ |
|CLI + pre-commit | ✅ | ❌ |
|Roles hierarchy | ✅ | ❌ |
-|Custom messages | ✅ | ❌ |
+|Personalized feedbacks | ✅ | ❌ |
+|Customizable config | ✅ | ❌ |
+|Commit blockage | ✅ | ❌ |
|Godfather vibe | ✅ | ❌ |
+
## SCREENSHOTS
@@ -52,23 +65,18 @@ npm install @donedeal0/codefather --save-dev
## USAGE
-**Codefather** has 4 commands:
+**Codefather** has 3 commands:
-```bash
-# checks if your access rules are respected in your repository
-codefather
-
-# creates a default config.ts at the root of your repository and add a `codefather` command to your package.json
-codefather-init
-
-# creates a default config.json at the root of your repository and add a `codefather` command to your package.json
-codefather-init -- --json
-
-# similar to the `codefather` command, but works in a Github Action environment
-codefather-github
-```
+- `codefather`: checks if your access rules are respected in your repository.
+- `codefather-init`: creates a default config at the root of your repository and adds a `codefather` command to your `package.json`.
+ - If a `.github/CODEOWNERS` file is present, it will be used to generate the config.
+ - Accepts two optional flags:
+ - `json`: generates a json config file instead of a `ts` one.
+ - `overwrite`: overwrite an existing codefather config.
+ - example: `npm run codefather-init json overwrite`
+- `codefather-github`: similar to `codefather`, but designed to run in a GitHub Action environment
-You can either add a script shortcut in your `package.json` (recommended):
+You can either add a script shortcut in your `package.json`:
```json
"scripts": {
@@ -91,26 +99,17 @@ At the root of your repository, add a `codefather.ts` or `codefather.json` file.
import type { CodefatherConfig } from "@donedeal0/codefather";
export default {
- caporegimes: [
- { name: "solozzo" },
- { name: "@lucabrasi", emailPrefix: "luca.brasi" },
- ],
+ caporegimes: [{ name: "solozzo" }, { name: "lucabrasi" }],
rules: [
{
match: ["package.json", "src/core/**", /^src\/app\/.*\.css$/],
- goodfellas: [
- { name: "solozzo" },
- { name: "@tomhagen", emailPrefix: "tom.hagen" },
- ],
+ goodfellas: [{ name: "solozzo" }, { name: "tomhagen" }],
crews: ["clemenzaPeople"],
allowForgiveness: false,
},
{
match: ["src/models/**"],
- goodfellas: [
- { name: "mike", emailPrefix: "michael.corleone" },
- { name: "sonny", emailPrefix: "sonny" },
- ],
+ goodfellas: [{ name: "mike" }, { name: "sonny" }],
allowForgiveness: true,
message: "Custom message to tell you to NOT TOUCH THE MODELS!",
},
@@ -124,7 +123,7 @@ export default {
autoAssignCaporegimes: true,
},
crews: {
- clemenzaPeople: [{ name: "@paulieGatto" }, { name: "@lucabrasi" }],
+ clemenzaPeople: [{ name: "paulieGatto" }, { name: "lucabrasi" }],
},
} satisfies CodefatherConfig;
```
@@ -135,11 +134,8 @@ export default {
```ts
type CodefatherConfig {
- /** List of users authorized to modify any files in your repository.
- * name: github username.
- * emailPrefix: prefix of the user email tied to their Github account (e.g. johnny.fontane@jazz.com should be johnny.fontane).
- */
- caporegimes?: GitUser[];
+ /** List of users authorized to modify any files in your repository. */
+ caporegimes?: {name: string}[];
/** Rules that apply to protected files and folders */
rules: CodefatherRule[];
/** Options to refine the output */
@@ -153,11 +149,11 @@ type CodefatherConfig {
codeReviews?: {
/** If true, goodfellas responsible for modified files will be assigned on relevant pull requests, except the committers. Defaults to true. */
autoAssignGoodfellas: boolean;
- /** If true, caporegimes will be assigned on every pull request except the committers. Defaults to false. */
+ /** If true, caporegimes will be assigned on every pull request, except the committers. Defaults to false. */
autoAssignCaporegimes: boolean;
};
/** Group users into teams. Crew names and composition are flexible in CLI mode but should match your github teams if used in a Github Action */
- crews?: Record;
+ crews?: Record;
}
```
@@ -167,13 +163,10 @@ type CodefatherConfig {
type CodefatherRule {
/** List of the files or folders that can only be modified by a given list of users */
match: Array;
- /** List of users authorized to modify the list of files or folders.
- * name: github username.
- * emailPrefix: prefix of the user email tied to their Github account (e.g. johnny.fontane@jazz.com should be johnny.fontane) .
- */
- goodfellas: GitUser[];
- /** List of authorized user crews. The crews must be defined at the root of your config when used in CLI mode. */
- crews?: CrewName[];
+ /** List of users authorized to modify the list of files or folders. */
+ goodfellas: {name: string}[];
+ /** List of authorized user crews (teams). The crews must be defined at the root of your config when used in CLI mode. */
+ crews?: string[];
/** The message displayed if an unauthorized user tries to modify a protected file. If empty, a random message will be generated. */
message?: string;
/** If true, a warning will be issued and the script will not throw an error. False by default. */
@@ -181,41 +174,20 @@ type CodefatherRule {
}
```
-> A `GitUser` is a developer in your codebase:
-
-```ts
-type GitUser = {
- name?: string;
- emailPrefix?: string;
-};
-```
-
-You can use either the name, the email, or both, depending on your preference. The name should match your GitHub username (e.g. `@tom.hagen`). If you prefer the email, it should also be tied to your Github account.
-
-For security reasons, only the email prefix is allowed in your config (e.g. `johnny.fontane@jazz.com` should be `johnny.fontane`).
-
-In CLI mode, the name and email are retrieved from your Git config. You can set them like this:
+The name should match your GitHub username (e.g. `tomhagen`). In CLI mode, the name is retrieved from your Git config. You can set it like this:
```bash
git config --global user.username "DonCorleone"
- git config --global user.email "vito.corleone@nyc.com"
```
-You can verify the current values like this:
+You can verify the current value like this:
```bash
git config user.username # return DonCorleone
-git config user.email # return vito.corleone@nyc.com
```
In a Github Action, `codefather` will use Github's API, so you don't have to worry about the git config.
-> A `CrewName` is the name of a developers team
-
-```ts
-type CrewName = string;
-```
-
# GITHUB ACTION
@@ -254,18 +226,20 @@ jobs:
To enforce reviews from codeowners (goodfellas, caporegimes and crews), consider enabling branch protection in your repository settings. To do it:
- Go to `settings`
-- Click on `Branches`on the left sidebar
+- Click on `Branches` on the left sidebar
- Select `Add classic branch protection rule`
- Check
- `Require a pull request before merging`
- `Require approvals`
-- You're now under the protection of the Codefather.
+ - `Require review from Code Owners`
+ - `Require status checks to pass before merging`
+- ✅ You're now under the protection of the Codefather.
# GLOSSARY
-Codefather uses the Godfather's lingo. Although you don't need to know it to use the library, here are the definition of the special words used in the config file:
+**Codefather** uses the Godfather's lingo. Although you don't need to know it to use the library, here are the definition of the special words used in the config file:
- `caporegime`: a captain who leads a group of mafia members. It's a tech-lead.
- `goodfella`: an appellation for a mobster (like "wise-guy" or "made man"). It's a developer.
@@ -280,7 +254,7 @@ This being said, if you don't like the gangster movie atmosphere and still want
## CREDITS
-DoneDeal0 | talk.donedeal0[at]gmail.com
+DoneDeal0 | talk.donedeal0@gmail.com
## SUPPORT
diff --git a/cli/git-user/git-user.test.ts b/cli/git-user/git-user.test.ts
index 95150b7..9c872bd 100644
--- a/cli/git-user/git-user.test.ts
+++ b/cli/git-user/git-user.test.ts
@@ -17,46 +17,20 @@ describe("getLocalGitUser", () => {
jest.clearAllMocks();
});
- it("returns an user with both email and username", () => {
- (execSync as jest.Mock)
- .mockReturnValueOnce("tom.hagen@don.com")
- .mockReturnValueOnce("@tomhagen");
- expect(getLocalGitUser()).toEqual({
- name: "@tomhagen",
- emailPrefix: "tom.hagen",
- });
- });
-
- it("returns an user with email only", () => {
- (execSync as jest.Mock)
- .mockReturnValueOnce("tom.hagen@don.com")
- .mockReturnValueOnce(undefined);
- expect(getLocalGitUser()).toEqual({
- name: undefined,
- emailPrefix: "tom.hagen",
- });
- });
-
- it("returns an user with username only", () => {
- (execSync as jest.Mock)
- .mockReturnValueOnce(undefined)
- .mockReturnValueOnce("@tomhagen");
- expect(getLocalGitUser()).toEqual({
- name: "@tomhagen",
- emailPrefix: undefined,
- });
+ it("returns an user with an username", () => {
+ (execSync as jest.Mock).mockReturnValueOnce("tomhagen");
+ expect(getLocalGitUser()).toEqual({ name: "tomhagen" });
});
it("throws an error if execSync fails", () => {
(execSync as jest.Mock).mockImplementationOnce(() => {
- throw new Error("No email config");
+ throw new Error();
});
expect(() => getLocalGitUser()).toThrow("Error message");
});
- it("throws an error if there is no email and no username in the git config", () => {
- (execSync as jest.Mock)
- .mockReturnValueOnce(undefined)
- .mockReturnValueOnce(undefined);
+
+ it("throws an error if there is no username in the git config", () => {
+ (execSync as jest.Mock).mockReturnValueOnce(undefined);
expect(() => getLocalGitUser()).toThrow();
});
});
diff --git a/cli/git-user/index.ts b/cli/git-user/index.ts
index 8539b96..655c8e9 100644
--- a/cli/git-user/index.ts
+++ b/cli/git-user/index.ts
@@ -1,21 +1,16 @@
import { execSync } from "child_process";
-import { getEmailPrefix } from "@shared/formatter";
import { getRandomMessage } from "@shared/messages";
import { GitUser, MessageType } from "@shared/models";
export function getLocalGitUser(): GitUser {
try {
- const email = execSync("git config user.email", {
- encoding: "utf8",
- });
const name = execSync("git config user.username", {
encoding: "utf8",
});
- if (!email && !name) {
+ if (!name) {
throw new Error();
}
- const emailPrefix = getEmailPrefix(email);
- return { name, emailPrefix };
+ return { name };
// eslint-disable-next-line @typescript-eslint/no-unused-vars
} catch (_: unknown) {
throw new Error(getRandomMessage(MessageType.NoGitConfig));
diff --git a/package-lock.json b/package-lock.json
index 57e1065..c762e9f 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,11 +1,11 @@
{
- "name": "codefather",
+ "name": "@donedeal0/codefather",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
- "name": "codefather",
+ "name": "@donedeal0/codefather",
"version": "1.0.0",
"license": "ISC",
"dependencies": {
diff --git a/package.json b/package.json
index 9742c8f..b395390 100644
--- a/package.json
+++ b/package.json
@@ -4,12 +4,14 @@
"description": "Codefather protects your codebase by controlling who can change what. Set authorization levels, lock down files, and enforce your rules—offline via CLI or online with GitHub Actions.",
"license": "ISC",
"author": "DoneDeal0",
- "files": ["dist"],
+ "files": [
+ "dist"
+ ],
"main": "dist/index.cjs",
"module": "dist/index.js",
"types": "dist/index.d.ts",
"publishConfig": {
- "access": "public"
+ "access": "public"
},
"exports": {
".": {
diff --git a/scripts/github/octokit/index.ts b/scripts/github/octokit/index.ts
index 0712f76..188c267 100644
--- a/scripts/github/octokit/index.ts
+++ b/scripts/github/octokit/index.ts
@@ -3,7 +3,6 @@ import { context, getOctokit } from "@actions/github";
import { GitHub } from "@actions/github/lib/utils";
import { RestEndpointMethodTypes } from "@octokit/rest";
import { matchFilesAgainstRule } from "@shared/file-matcher";
-import { getEmailPrefix } from "@shared/formatter";
import { CodefatherConfig, colorsMap, CrewName, GitUser } from "@shared/models";
type Commits =
@@ -20,27 +19,18 @@ export class Octokit {
}
private getUniqueCommittersList(commits: Commits): GitUser[] {
- return commits.reduce(
- (acc, { commit, committer }) => {
- const name = committer?.login;
- const emailPrefix = getEmailPrefix(commit.author?.email);
- if (!name && !emailPrefix) return acc;
- const alreadyAdded =
- acc.nameMap.has(name) || acc.emailMap.has(emailPrefix);
- if (!alreadyAdded) {
- const committer: GitUser = { name, emailPrefix };
- if (name) acc.nameMap.add(name);
- if (emailPrefix) acc.emailMap.add(emailPrefix);
- acc.list.push(committer);
- }
- return acc;
- },
- {
- nameMap: new Set(),
- emailMap: new Set(),
- list: [] as GitUser[],
+ const nameMap = new Set();
+ return commits.reduce((acc, { committer }) => {
+ const name = committer?.login;
+ if (!name) return acc;
+ const alreadyAdded = nameMap.has(name);
+ if (!alreadyAdded) {
+ const committer: GitUser = { name };
+ if (name) nameMap.add(name);
+ acc.push(committer);
}
- ).list;
+ return acc;
+ }, [] as GitUser[]);
}
public async getCommitters(
@@ -60,7 +50,7 @@ export class Octokit {
const committers = this.getUniqueCommittersList(commitsList as Commits);
if (committers.length === 0) {
throw new Error(
- "𐄂 Couldn’t find email or username in the commit author metadata."
+ "𐄂 The username could not be found in the commit author metadata."
);
}
return committers;
@@ -114,12 +104,8 @@ export class Octokit {
allReviewers.push(...(rule.goodfellas || []));
}
const validReviewers = allReviewers.filter(
- ({ name, emailPrefix }) =>
- !committers.some(
- (committer) =>
- (name && committer.name === name) ||
- (emailPrefix && committer.emailPrefix === emailPrefix)
- )
+ ({ name }) =>
+ !committers.some((committer) => name && committer.name === name)
);
const reviewers = [
...new Set(validReviewers.map(({ name }) => name).filter(Boolean)),
diff --git a/scripts/github/octokit/octokit.test.ts b/scripts/github/octokit/octokit.test.ts
index 44bc88a..d35f280 100644
--- a/scripts/github/octokit/octokit.test.ts
+++ b/scripts/github/octokit/octokit.test.ts
@@ -155,9 +155,7 @@ describe("Octokit", () => {
}));
const octokit = Octokit.init();
const committers = await octokit.getCommitters(88, false);
- expect(committers).toStrictEqual([
- { name: "mike", emailPrefix: "michael.corleone" },
- ]);
+ expect(committers).toStrictEqual([{ name: "mike" }]);
});
it("getCommitters - return the username and the prefix of all committers if vouchForAllCommitters is true", async () => {
// eslint-disable-next-line @typescript-eslint/no-require-imports
@@ -184,7 +182,7 @@ describe("Octokit", () => {
sha: "2",
author: { email: "tom.hagen@don.com" },
},
- committer: { login: "@tomhagen" },
+ committer: { login: "tomhagen" },
},
{
commit: {
@@ -201,10 +199,7 @@ describe("Octokit", () => {
}));
const octokit = Octokit.init();
const committers = await octokit.getCommitters(88);
- expect(committers).toStrictEqual([
- { name: "mike", emailPrefix: "michael.corleone" },
- { name: "@tomhagen", emailPrefix: "tom.hagen" },
- ]);
+ expect(committers).toStrictEqual([{ name: "mike" }, { name: "tomhagen" }]);
});
it("getCommitters - throw an error if there are no commits", async () => {
// eslint-disable-next-line @typescript-eslint/no-require-imports
@@ -263,7 +258,7 @@ describe("Octokit", () => {
await octokit.getCommitters(undefined, 88);
} catch (err) {
expect(err instanceof Error ? err.message : err).toBe(
- "𐄂 Couldn’t find email or username in the commit author metadata."
+ "𐄂 The username could not be found in the commit author metadata."
);
}
});
@@ -281,24 +276,15 @@ describe("Octokit", () => {
}),
}));
const config: CodefatherConfig = {
- caporegimes: [
- { name: "solozzo", emailPrefix: "solozzo" },
- { name: "@lucabrasi", emailPrefix: "luca.brasi" },
- ],
+ caporegimes: [{ name: "solozzo" }, { name: "lucabrasi" }],
rules: [
{
match: ["src/core/**"],
- goodfellas: [
- { name: "solozzo", emailPrefix: "solozzo-the-turk" },
- { name: "@tomhagen", emailPrefix: "tom.hagen" },
- ],
+ goodfellas: [{ name: "solozzo" }, { name: "tomhagen" }],
},
{
match: ["src/models/**"],
- goodfellas: [
- { name: "mike", emailPrefix: "michael.corleone" },
- { name: "sonny", emailPrefix: "sonny" },
- ],
+ goodfellas: [{ name: "mike" }, { name: "sonny" }],
},
],
codeReviews: {
@@ -306,10 +292,7 @@ describe("Octokit", () => {
autoAssignCaporegimes: false,
},
};
- const committers = [
- { name: "oldblueeyes", emailPrefix: "johnny.fontane" },
- { name: "@tom", emailPrefix: "tom.woltz" },
- ];
+ const committers = [{ name: "oldblueeyes" }, { name: "tom" }];
const updatedFiles = [resolve(process.cwd(), "src/core/index.ts")];
const octokit = Octokit.init();
await octokit.assignReviewers(88, updatedFiles, committers, config);
@@ -317,7 +300,7 @@ describe("Octokit", () => {
owner: "don",
repo: "empire",
pull_number: 88,
- reviewers: ["solozzo", "@tomhagen"],
+ reviewers: ["solozzo", "tomhagen"],
});
});
it("assignReviewers - assign relevant goodfellas and caporegimes as reviewers if autoAssignCaporegimes is true and don't duplicate reviewers", async () => {
@@ -334,24 +317,15 @@ describe("Octokit", () => {
}),
}));
const config: CodefatherConfig = {
- caporegimes: [
- { name: "solozzo", emailPrefix: "solozzo" },
- { name: "@lucabrasi", emailPrefix: "luca.brasi" },
- ],
+ caporegimes: [{ name: "solozzo" }, { name: "lucabrasi" }],
rules: [
{
match: ["src/core/**"],
- goodfellas: [
- { name: "solozzo", emailPrefix: "solozzo-the-turk" },
- { name: "@tomhagen", emailPrefix: "tom.hagen" },
- ],
+ goodfellas: [{ name: "solozzo" }, { name: "tomhagen" }],
},
{
match: ["src/models/**"],
- goodfellas: [
- { name: "mike", emailPrefix: "michael.corleone" },
- { name: "sonny", emailPrefix: "sonny" },
- ],
+ goodfellas: [{ name: "mike" }, { name: "sonny" }],
},
],
codeReviews: {
@@ -359,10 +333,7 @@ describe("Octokit", () => {
autoAssignCaporegimes: true,
},
};
- const committers = [
- { name: "oldblueeyes", emailPrefix: "johnny.fontane" },
- { name: "@tom", emailPrefix: "tom.woltz" },
- ];
+ const committers = [{ name: "oldblueeyes" }, { name: "tom" }];
const updatedFiles = [resolve(process.cwd(), "src/core/index.ts")];
const octokit = Octokit.init();
await octokit.assignReviewers(88, updatedFiles, committers, config);
@@ -370,7 +341,7 @@ describe("Octokit", () => {
owner: "don",
repo: "empire",
pull_number: 88,
- reviewers: ["solozzo", "@lucabrasi", "@tomhagen"],
+ reviewers: ["solozzo", "lucabrasi", "tomhagen"],
});
});
it("assignReviewers - assign a crew as reviewer", async () => {
@@ -387,25 +358,16 @@ describe("Octokit", () => {
}),
}));
const config: CodefatherConfig = {
- caporegimes: [
- { name: "solozzo", emailPrefix: "solozzo" },
- { name: "@lucabrasi", emailPrefix: "luca.brasi" },
- ],
+ caporegimes: [{ name: "solozzo" }, { name: "lucabrasi" }],
rules: [
{
match: ["src/core/**"],
- goodfellas: [
- { name: "solozzo", emailPrefix: "solozzo-the-turk" },
- { name: "@tomhagen", emailPrefix: "tom.hagen" },
- ],
+ goodfellas: [{ name: "solozzo" }, { name: "tomhagen" }],
crews: ["clemenzaPeople"],
},
{
match: ["src/models/**"],
- goodfellas: [
- { name: "mike", emailPrefix: "michael.corleone" },
- { name: "sonny", emailPrefix: "sonny" },
- ],
+ goodfellas: [{ name: "mike" }, { name: "sonny" }],
},
],
codeReviews: {
@@ -413,10 +375,10 @@ describe("Octokit", () => {
autoAssignCaporegimes: true,
},
crews: {
- clemenzaPeople: [{ name: "@paulieGatto" }, { name: "@lucabrasi" }],
+ clemenzaPeople: [{ name: "paulieGatto" }, { name: "lucabrasi" }],
},
};
- const committers = [{ name: "oldblueeyes", emailPrefix: "johnny.fontane" }];
+ const committers = [{ name: "oldblueeyes" }];
const updatedFiles = [resolve(process.cwd(), "src/core/index.ts")];
const octokit = Octokit.init();
await octokit.assignReviewers(88, updatedFiles, committers, config);
@@ -424,7 +386,7 @@ describe("Octokit", () => {
owner: "don",
repo: "empire",
pull_number: 88,
- reviewers: ["solozzo", "@lucabrasi", "@tomhagen"],
+ reviewers: ["solozzo", "lucabrasi", "tomhagen"],
team_reviewers: ["clemenzaPeople"],
});
});
@@ -442,25 +404,19 @@ describe("Octokit", () => {
}),
}));
const config: CodefatherConfig = {
- caporegimes: [
- { name: "solozzo", emailPrefix: "solozzo" },
- { name: "@lucabrasi", emailPrefix: "luca.brasi" },
- ],
+ caporegimes: [{ name: "solozzo" }, { name: "lucabrasi" }],
rules: [
{
match: ["src/core/**"],
goodfellas: [
- { name: "oldblueeyes", emailPrefix: "johnny.fontane" },
- { name: "solozzo", emailPrefix: "solozzo-the-turk" },
- { name: "@tomhagen", emailPrefix: "tom.hagen" },
+ { name: "oldblueeyes" },
+ { name: "solozzo" },
+ { name: "tomhagen" },
],
},
{
match: ["src/models/**"],
- goodfellas: [
- { name: "mike", emailPrefix: "michael.corleone" },
- { name: "sonny", emailPrefix: "sonny" },
- ],
+ goodfellas: [{ name: "mike" }, { name: "sonny" }],
},
],
codeReviews: {
@@ -469,9 +425,9 @@ describe("Octokit", () => {
},
};
const committers = [
- { name: "oldblueeyes", emailPrefix: "johnny.fontane" },
- { name: "@lucabrasi", emailPrefix: "luca.brasi" },
- { name: "@tom", emailPrefix: "tom.woltz" },
+ { name: "oldblueeyes" },
+ { name: "lucabrasi" },
+ { name: "tom" },
];
const updatedFiles = [resolve(process.cwd(), "src/core/index.ts")];
const octokit = Octokit.init();
@@ -480,7 +436,7 @@ describe("Octokit", () => {
owner: "don",
repo: "empire",
pull_number: 88,
- reviewers: ["solozzo", "@tomhagen"],
+ reviewers: ["solozzo", "tomhagen"],
});
});
it("assignReviewers - assign reviewers across multiple rules", async () => {
@@ -497,31 +453,25 @@ describe("Octokit", () => {
}),
}));
const config: CodefatherConfig = {
- caporegimes: [
- { name: "solozzo", emailPrefix: "solozzo" },
- { name: "@lucabrasi", emailPrefix: "luca.brasi" },
- ],
+ caporegimes: [{ name: "solozzo" }, { name: "lucabrasi" }],
rules: [
{
match: ["src/core/**"],
goodfellas: [
- { name: "oldblueeyes", emailPrefix: "johnny.fontane" },
- { name: "solozzo", emailPrefix: "solozzo-the-turk" },
- { name: "@tomhagen", emailPrefix: "tom.hagen" },
+ { name: "oldblueeyes" },
+ { name: "solozzo" },
+ { name: "tomhagen" },
],
crews: ["clemenzaPeople", "tessioTeam"],
},
{
match: ["src/models/**"],
- goodfellas: [
- { name: "mike", emailPrefix: "michael.corleone" },
- { name: "sonny", emailPrefix: "sonny" },
- ],
+ goodfellas: [{ name: "mike" }, { name: "sonny" }],
crews: ["tessioTeam"],
},
{
match: ["src/utils/**"],
- goodfellas: [{ name: "@tom", emailPrefix: "tom.woltz" }],
+ goodfellas: [{ name: "tom" }],
},
],
codeReviews: {
@@ -529,11 +479,11 @@ describe("Octokit", () => {
autoAssignCaporegimes: false,
},
crews: {
- clemenzaPeople: [{ name: "@paulieGatto" }, { name: "@lucabrasi" }],
+ clemenzaPeople: [{ name: "paulieGatto" }, { name: "lucabrasi" }],
tessioTeam: [{ name: "salvatore" }],
},
};
- const committers = [{ name: "fredo", emailPrefix: "frederico.corleone" }];
+ const committers = [{ name: "fredo" }];
const updatedFiles = [
resolve(process.cwd(), "src/core/index.ts"),
resolve(process.cwd(), "src/models/message.ts"),
@@ -544,7 +494,7 @@ describe("Octokit", () => {
owner: "don",
repo: "empire",
pull_number: 88,
- reviewers: ["oldblueeyes", "solozzo", "@tomhagen", "mike", "sonny"],
+ reviewers: ["oldblueeyes", "solozzo", "tomhagen", "mike", "sonny"],
team_reviewers: ["clemenzaPeople", "tessioTeam"],
});
});
@@ -562,24 +512,15 @@ describe("Octokit", () => {
}),
}));
const config: CodefatherConfig = {
- caporegimes: [
- { name: "solozzo", emailPrefix: "solozzo" },
- { name: "@lucabrasi", emailPrefix: "luca.brasi" },
- ],
+ caporegimes: [{ name: "solozzo" }, { name: "lucabrasi" }],
rules: [
{
match: ["src/core/**"],
- goodfellas: [
- { name: "solozzo", emailPrefix: "solozzo-the-turk" },
- { name: "@tomhagen", emailPrefix: "tom.hagen" },
- ],
+ goodfellas: [{ name: "solozzo" }, { name: "tomhagen" }],
},
{
match: ["src/models/**"],
- goodfellas: [
- { name: "mike", emailPrefix: "michael.corleone" },
- { name: "sonny", emailPrefix: "sonny" },
- ],
+ goodfellas: [{ name: "mike" }, { name: "sonny" }],
},
],
codeReviews: {
@@ -587,10 +528,7 @@ describe("Octokit", () => {
autoAssignCaporegimes: false,
},
};
- const committers = [
- { name: "oldblueeyes", emailPrefix: "johnny.fontane" },
- { name: "@tom", emailPrefix: "tom.woltz" },
- ];
+ const committers = [{ name: "oldblueeyes" }, { name: "tom" }];
const updatedFiles = [
resolve(process.cwd(), "src/utils/index.ts"),
resolve(process.cwd(), ".env"),
diff --git a/scripts/github/run-github-check/index.ts b/scripts/github/run-github-check/index.ts
index ac6eb47..923378b 100644
--- a/scripts/github/run-github-check/index.ts
+++ b/scripts/github/run-github-check/index.ts
@@ -1,3 +1,5 @@
+import fs from "fs";
+import path from "path";
import { showDonAscii } from "@shared/ascii/don";
import { validateFiles } from "@shared/file-validator";
import { loadConfig } from "@shared/loader";
@@ -34,7 +36,14 @@ export async function runGithubCheck() {
const results = validateFiles(files, committers, config);
const addReviewers = config.codeReviews?.autoAssignGoodfellas ?? true;
- if (addReviewers || !!config.codeReviews?.autoAssignCaporegimes) {
+ const hasCodeownersFile = fs.existsSync(
+ path.resolve(process.cwd(), "./.github/CODEOWNERS")
+ );
+
+ if (
+ !hasCodeownersFile &&
+ (addReviewers || !!config.codeReviews?.autoAssignCaporegimes)
+ ) {
await octokit.assignReviewers(pullRequestID, files, committers, config);
}
diff --git a/scripts/github/run-github-check/run-github-check.test.ts b/scripts/github/run-github-check/run-github-check.test.ts
index 44a4b2e..e628f31 100644
--- a/scripts/github/run-github-check/run-github-check.test.ts
+++ b/scripts/github/run-github-check/run-github-check.test.ts
@@ -1,4 +1,6 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
+import { writeFileSync, unlinkSync } from "fs";
+import { resolve } from "path";
import { showDonAscii } from "@shared/ascii/don";
import { validateFiles } from "@shared/file-validator";
import { loadConfig } from "@shared/loader";
@@ -206,6 +208,33 @@ describe("runGithubCheck", () => {
expect(assignReviewersMock).not.toHaveBeenCalled();
});
+ test("doesn't assign reviewers if there is a codeowner file", async () => {
+ process.env.CI = "true";
+ process.env.GITHUB_TOKEN = "token";
+ const codeOwnersPath = resolve(process.cwd(), "./.github/CODEOWNERS");
+ writeFileSync(
+ codeOwnersPath,
+ `
+ /src/core @corleone/caporegimes
+ /src/models tomhagen @solozzo
+ `
+ );
+ (loadConfig as jest.Mock).mockResolvedValue({
+ caporegimes: [],
+ rules: [],
+ options: { showAscii: false },
+ codeReviews: {
+ autoAssignGoodfellas: true,
+ autoAssignCaporegimes: true,
+ },
+ });
+
+ (validateFiles as jest.Mock).mockReturnValue(defaultResults);
+ await runGithubCheck();
+ unlinkSync(codeOwnersPath);
+ expect(assignReviewersMock).not.toHaveBeenCalled();
+ });
+
test("exits with error on unknown exception", async () => {
process.env.CI = "true";
process.env.GITHUB_TOKEN = "token";
diff --git a/scripts/init/generate-from-codeowner/generate-from-codeowner.test.ts b/scripts/init/generate-from-codeowner/generate-from-codeowner.test.ts
new file mode 100644
index 0000000..369d947
--- /dev/null
+++ b/scripts/init/generate-from-codeowner/generate-from-codeowner.test.ts
@@ -0,0 +1,117 @@
+import fs from "fs";
+import { generateConfigFromCodeowners } from ".";
+
+jest.mock("fs", () => ({
+ promises: {
+ readFile: jest.fn(),
+ },
+}));
+
+describe("generateConfigFromCodeowners", () => {
+ it("return a codefather config from a codeowner file", async () => {
+ (fs.promises.readFile as jest.Mock).mockReturnValue(`
+ # Github doc about CODEOWNERS is available at https://docs.github.com
+
+# Each line is a file pattern followed by one or more owners.
+
+# App leads
+
+turbo.json @corleone/clemenzaPeople
+/.github/ @corleone/clemenzaPeople
+**/package.json @corleone/clemenzaPeople
+tsconfig.json @corleone/tessioTeam
+
+# Application owners
+
+/apps/gambling/ @corleone/clemenzaPeople @tomhagen @mike
+/apps/protection @tomhagen @oldblueeyes`);
+
+ const { config, crews } =
+ await generateConfigFromCodeowners("codeowners-path");
+ expect(config).toStrictEqual({
+ codeReviews: {
+ autoAssignGoodfellas: true,
+ },
+ crews: {
+ clemenzaPeople: [],
+ tessioTeam: [],
+ },
+ rules: [
+ {
+ match: ["turbo.json", "/.github/", "**/package.json"],
+ goodfellas: [],
+ crews: ["clemenzaPeople"],
+ },
+ {
+ match: ["tsconfig.json"],
+ goodfellas: [],
+ crews: ["tessioTeam"],
+ },
+ {
+ match: ["/apps/gambling/"],
+ goodfellas: [{ name: "mike" }, { name: "tomhagen" }],
+ crews: ["clemenzaPeople"],
+ },
+ {
+ match: ["/apps/protection"],
+ goodfellas: [{ name: "oldblueeyes" }, { name: "tomhagen" }],
+ },
+ ],
+ });
+ expect(crews).toStrictEqual(["clemenzaPeople", "tessioTeam"]);
+ });
+ it("return a codefather config from a codeowner file without crews", async () => {
+ (fs.promises.readFile as jest.Mock).mockReturnValue(`
+ # Github doc about CODEOWNERS is available at https://docs.github.com
+
+# Each line is a file pattern followed by one or more owners.
+
+# Application owners
+
+/apps/gambling/ @tomhagen @mike
+/apps/protection @tomhagen @oldblueeyes`);
+
+ const { config, crews } =
+ await generateConfigFromCodeowners("codeowners-path");
+ expect(config).toStrictEqual({
+ codeReviews: {
+ autoAssignGoodfellas: true,
+ },
+ rules: [
+ {
+ match: ["/apps/gambling/"],
+ goodfellas: [{ name: "mike" }, { name: "tomhagen" }],
+ },
+ {
+ match: ["/apps/protection"],
+ goodfellas: [{ name: "oldblueeyes" }, { name: "tomhagen" }],
+ },
+ ],
+ crews: {},
+ });
+ expect(crews).toStrictEqual([]);
+ });
+ it("return a codefather config from an empty codeowner file", async () => {
+ (fs.promises.readFile as jest.Mock).mockReturnValue("");
+
+ const { config, crews } =
+ await generateConfigFromCodeowners("codeowners-path");
+ expect(config).toStrictEqual({
+ codeReviews: {
+ autoAssignGoodfellas: true,
+ },
+ rules: [],
+ crews: {},
+ });
+ expect(crews).toStrictEqual([]);
+ });
+ it("throws if the codeowner file is invalid", async () => {
+ (fs.promises.readFile as jest.Mock).mockImplementationOnce(() => {
+ throw new Error("𐄂 Your CODEOWNER file is invalid.");
+ });
+
+ await expect(
+ generateConfigFromCodeowners("codeowners-path")
+ ).rejects.toThrow("𐄂 Your CODEOWNER file is invalid.");
+ });
+});
diff --git a/scripts/init/generate-from-codeowner/index.ts b/scripts/init/generate-from-codeowner/index.ts
new file mode 100644
index 0000000..d891e7d
--- /dev/null
+++ b/scripts/init/generate-from-codeowner/index.ts
@@ -0,0 +1,87 @@
+import fs from "fs";
+import {
+ CodefatherConfig,
+ CodefatherRule,
+ CrewName,
+ GitUser,
+} from "@shared/models";
+
+function isCrewOwner(owner: string): owner is CrewName {
+ return owner.startsWith("@") && owner.includes("/");
+}
+
+function getValidCrewName(crew: string): string {
+ const parts = crew.replace(/^@/, "").split("/");
+ return parts.length > 1 ? parts[1] : parts[0] || crew;
+}
+
+function getValidGoodfella(goodfella: string): string {
+ return goodfella.replace(/^@+/, "").split("/")[0] || goodfella;
+}
+
+async function parseCodeowners(filePath: string): Promise {
+ const content = await fs.promises.readFile(filePath, "utf-8");
+ const lines = content.split("\n");
+
+ const ownersCombos = new Map();
+
+ for (const rawLine of lines) {
+ const line = rawLine.trim();
+ if (line.startsWith("#")) continue;
+ const [match, ...owners] = line.split(/\s+/);
+ if (match && owners.length > 0) {
+ const ownersCombo = owners.sort().join(",");
+ if (!ownersCombos.has(ownersCombo)) {
+ ownersCombos.set(ownersCombo, [match]);
+ } else {
+ const data = ownersCombos.get(ownersCombo) || [];
+ ownersCombos.set(ownersCombo, [...data, match]);
+ }
+ }
+ }
+
+ const rules: CodefatherRule[] = [];
+
+ for (const [owners, match] of ownersCombos.entries()) {
+ const crews: CrewName[] = [];
+ const goodfellas: GitUser[] = [];
+
+ owners.split(",").forEach((owner) => {
+ const validOwner = owner.trim();
+ if (isCrewOwner(validOwner)) {
+ crews.push(getValidCrewName(validOwner));
+ } else {
+ goodfellas.push({ name: getValidGoodfella(validOwner) });
+ }
+ });
+
+ rules.push({ match, goodfellas, ...(crews.length > 0 ? { crews } : {}) });
+ }
+ return rules;
+}
+
+function generateConfig(
+ rules: CodefatherRule[],
+ crews: CrewName[]
+): CodefatherConfig {
+ return {
+ rules,
+ codeReviews: { autoAssignGoodfellas: true },
+ crews: crews.reduce((acc, crew) => ({ ...acc, [crew]: [] }), {}),
+ };
+}
+
+export async function generateConfigFromCodeowners(
+ codeownersFile: string
+): Promise<{ config: CodefatherConfig; crews: CrewName[] }> {
+ try {
+ const rules = await parseCodeowners(codeownersFile);
+ const crews = [
+ ...new Set(rules.flatMap((rule) => rule.crews).filter(Boolean)),
+ ] as CrewName[];
+ return { config: generateConfig(rules, crews), crews };
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ } catch (_: unknown) {
+ throw new Error("𐄂 Your CODEOWNER file is invalid.");
+ }
+}
diff --git a/scripts/init/index.ts b/scripts/init/index.ts
index 9639f4c..fc844f4 100644
--- a/scripts/init/index.ts
+++ b/scripts/init/index.ts
@@ -1,71 +1,125 @@
#!/usr/bin/env node
import fs from "fs";
import path from "path";
-import { colorsMap } from "@shared/models";
+import { CodefatherConfig, colorsMap, CrewName } from "@shared/models";
import { safeJSONParse } from "@shared/parser";
+import { generateConfigFromCodeowners } from "./generate-from-codeowner";
-const args = process.argv.slice(2);
-const useJson = args.includes("--json");
-
-export function runInit() {
- const rootDir = process.cwd();
- const configPath = path.join(
- rootDir,
- useJson ? "codefather.json" : "codefather.ts"
- );
- const pkgPath = path.join(rootDir, "package.json");
-
- if (!fs.existsSync(configPath)) {
- if (useJson) {
- fs.writeFileSync(configPath, JSON.stringify({ rules: [] }, null, 2));
- } else {
- fs.writeFileSync(
- configPath,
- `import type { CodefatherConfig } from "@donedeal0/codefather";\n\n` +
- `export default { rules: [] } satisfies CodefatherConfig;\n`
- );
- }
+function informFileCreated(
+ configPath: string,
+ isCodeownersBased: boolean,
+ crews: CrewName[],
+ canOverwrite: boolean
+) {
+ if (canOverwrite) {
console.log(
colorsMap.info,
- `- A ${path.basename(configPath)} config file has been created.`
+ `- A new ${configPath} config file has been created, your former config was overwritten.`
);
} else {
console.log(
colorsMap.info,
- `- A ${path.basename(configPath)} file already exists.`
+ `- A ${configPath} config file has been created.`
);
}
-
- if (fs.existsSync(pkgPath)) {
- const pkg = safeJSONParse<{
- scripts?: Record;
- }>(fs.readFileSync(pkgPath, "utf-8"));
-
- pkg.scripts = pkg.scripts || {};
- if (!pkg.scripts.codefather) {
- pkg.scripts.codefather = "codefather";
- fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2));
+ if (isCodeownersBased) {
+ console.log(
+ colorsMap.info,
+ `- The rules were filled with your CODEOWNER file.`
+ );
+ if (crews?.length > 0) {
console.log(
- colorsMap.info,
- "- A codefather script has been added to your package.json."
+ colorsMap.warning,
+ `- The following crews were detected:\n${crews
+ .map((crew) => ` - ${crew}`)
+ .join("\n")}
+Please specify their members in the codefather config for CLI enforcement.`
+ );
+ }
+ }
+}
+
+export async function runInit() {
+ try {
+ const args = process.argv.slice(2);
+ const useJson = args.includes("json");
+ const canOverwrite = args.includes("overwrite");
+ const rootDir = process.cwd();
+ const codeownersPath = path.join(rootDir, "./.github/CODEOWNERS");
+ let baseConfig: CodefatherConfig = { rules: [] };
+ let isCodeownersBased = false;
+ const crews: CrewName[] = [];
+ if (fs.existsSync(codeownersPath)) {
+ const data = await generateConfigFromCodeowners(codeownersPath);
+ baseConfig = data.config;
+ isCodeownersBased = true;
+ crews.push(...data.crews);
+ }
+ const configPath = path.join(
+ rootDir,
+ useJson ? "codefather.json" : "codefather.ts"
+ );
+ const pkgPath = path.join(rootDir, "package.json");
+ const config = JSON.stringify(baseConfig, null, 2);
+ if (!fs.existsSync(configPath) || canOverwrite) {
+ if (useJson) {
+ fs.writeFileSync(configPath, config);
+ } else {
+ fs.writeFileSync(
+ configPath,
+ `import type { CodefatherConfig } from "@donedeal0/codefather";\n\n` +
+ `export default ${config} satisfies CodefatherConfig;\n`
+ );
+ }
+ informFileCreated(
+ path.basename(configPath),
+ isCodeownersBased,
+ crews,
+ canOverwrite
);
} else {
console.log(
colorsMap.info,
- "- A codefather script already exists in your package.json."
+ `- A ${path.basename(configPath)} file already exists.`
);
}
- } else {
+
+ if (fs.existsSync(pkgPath)) {
+ const pkg = safeJSONParse<{
+ scripts?: Record;
+ }>(fs.readFileSync(pkgPath, "utf-8"));
+
+ pkg.scripts = pkg.scripts || {};
+ if (!pkg.scripts.codefather) {
+ pkg.scripts.codefather = "codefather";
+ fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2));
+ console.log(
+ colorsMap.info,
+ "- A codefather script has been added to your package.json."
+ );
+ } else {
+ console.log(
+ colorsMap.info,
+ "- A codefather script already exists in your package.json."
+ );
+ }
+ } else {
+ return console.log(
+ colorsMap.error,
+ "⚠ No package.json found in the project root. Skipping script setup."
+ );
+ }
+
+ return console.log(
+ colorsMap.success,
+ "\n✓ Setup complete. Run `npm run codefather` to enforce your rules."
+ );
+ } catch (err: unknown) {
return console.log(
colorsMap.error,
- "⚠️ No package.json found in the project root. Skipping script setup."
+ err instanceof Error ? err.message : String(err)
);
}
-
- return console.log(
- colorsMap.success,
- "\n✓ Setup complete. Run `npm run codefather` to enforce your rules."
- );
}
if (import.meta.url === new URL(import.meta.url).href) {
diff --git a/scripts/init/init.test.ts b/scripts/init/init.test.ts
index b4997d8..8dfadf3 100644
--- a/scripts/init/init.test.ts
+++ b/scripts/init/init.test.ts
@@ -1,20 +1,24 @@
import fs from "fs";
import { resolve } from "path";
import { colorsMap } from "@shared/models";
+import { generateConfigFromCodeowners } from "./generate-from-codeowner";
import { runInit } from ".";
jest.mock("fs");
+jest.mock("./generate-from-codeowner");
const originalArgv = process.argv;
describe("codefather init script", () => {
const mockConsole = jest.spyOn(console, "log").mockImplementation(() => {});
(fs.existsSync as jest.Mock).mockImplementation(() => true);
const configPath = resolve("project", "codefather.ts");
+ const jsonConfigPath = resolve("project", "codefather.json");
const pkgPath = resolve("project", "package.json");
+ const codeownersPath = resolve("project", "./.github/CODEOWNERS");
beforeEach(() => {
jest.clearAllMocks();
- process.argv = ["node", "init.js"]; // no --json flag by default
+ process.argv = ["node", "init.js"]; // no flags by default
jest.spyOn(process, "cwd").mockReturnValue(resolve("project"));
});
@@ -22,7 +26,7 @@ describe("codefather init script", () => {
process.argv = originalArgv;
});
- test("creates codefather.ts if it does not exist", () => {
+ test("creates codefather.ts if it does not exist", async () => {
(fs.existsSync as jest.Mock).mockImplementation(
(p: string) => p === pkgPath
);
@@ -30,7 +34,7 @@ describe("codefather init script", () => {
JSON.stringify({ scripts: {} }, null, 2)
);
- runInit();
+ await runInit();
expect(fs.writeFileSync).toHaveBeenCalledWith(
configPath,
@@ -47,10 +51,133 @@ describe("codefather init script", () => {
);
});
- test("does not create codefather.ts if it already exists", () => {
- (fs.existsSync as jest.Mock).mockImplementation(() => true);
+ test("creates codefather.ts from codeowners if codeowners exists", async () => {
+ (fs.existsSync as jest.Mock).mockImplementation(
+ (p: string) => p !== configPath
+ );
+ (generateConfigFromCodeowners as jest.Mock).mockImplementation(() => ({
+ config: {
+ rules: [
+ { match: ["src/models/*"], goodfellas: ["tomhagen", "solozzo"] },
+ ],
+ },
+ crews: [],
+ }));
+ (fs.readFileSync as jest.Mock).mockReturnValue(
+ JSON.stringify({ scripts: {} }, null, 2)
+ );
+
+ await runInit();
+
+ expect(fs.writeFileSync).toHaveBeenCalledWith(
+ configPath,
+ expect.stringContaining("import type { CodefatherConfig }")
+ );
+ expect(fs.writeFileSync).toHaveBeenCalledWith(
+ configPath,
+ expect.stringContaining("export default")
+ );
+ expect(fs.writeFileSync).toHaveBeenCalledWith(
+ configPath,
+ expect.stringContaining("solozzo")
+ );
+ expect(fs.writeFileSync).toHaveBeenCalledWith(
+ configPath,
+ expect.stringContaining("tomhagen")
+ );
+
+ expect(mockConsole).toHaveBeenCalledWith(
+ colorsMap.info,
+ "- A codefather.ts config file has been created."
+ );
+ expect(mockConsole).toHaveBeenCalledWith(
+ colorsMap.info,
+ "- The rules were filled with your CODEOWNER file."
+ );
+ expect(mockConsole).not.toHaveBeenCalledWith(
+ colorsMap.info,
+ expect.stringContaining(`- The following crews were detected`)
+ );
+ });
+
+ test("creates codefather.ts from codeowners if codeowners exists and warns about crews", async () => {
+ (fs.existsSync as jest.Mock).mockImplementation(
+ (p: string) => p !== configPath
+ );
+ (generateConfigFromCodeowners as jest.Mock).mockImplementation(() => ({
+ config: {
+ rules: [
+ { match: ["src/models/*"], goodfellas: ["tomhagen", "solozzo"] },
+ ],
+ },
+ crews: ["clemenzaPeople"],
+ }));
+ (fs.readFileSync as jest.Mock).mockReturnValue(
+ JSON.stringify({ scripts: {} }, null, 2)
+ );
+
+ await runInit();
+
+ expect(fs.writeFileSync).toHaveBeenCalledWith(
+ configPath,
+ expect.stringContaining("import type { CodefatherConfig }")
+ );
+ expect(fs.writeFileSync).toHaveBeenCalledWith(
+ configPath,
+ expect.stringContaining("export default")
+ );
+
+ expect(mockConsole).toHaveBeenCalledWith(
+ colorsMap.info,
+ "- A codefather.ts config file has been created."
+ );
+ expect(mockConsole).toHaveBeenCalledWith(
+ colorsMap.info,
+ "- The rules were filled with your CODEOWNER file."
+ );
+
+ expect(mockConsole).toHaveBeenCalledWith(
+ colorsMap.warning,
+ expect.stringContaining(`- The following crews were detected`)
+ );
+ expect(mockConsole).toHaveBeenCalledWith(
+ colorsMap.warning,
+ expect.stringContaining(`clemenzaPeople`)
+ );
+ });
+
+ test("creates codefather.json if it does not exist and the json flag is provided", async () => {
+ process.argv = ["node", "init.js", "json"];
+ (fs.existsSync as jest.Mock).mockImplementation(
+ (p: string) => p === pkgPath
+ );
+ (fs.readFileSync as jest.Mock).mockReturnValue(
+ JSON.stringify({ scripts: {} }, null, 2)
+ );
+
+ await runInit();
+
+ expect(fs.writeFileSync).toHaveBeenCalledWith(
+ jsonConfigPath,
+ expect.not.stringContaining("import type { CodefatherConfig }")
+ );
+ expect(fs.writeFileSync).toHaveBeenCalledWith(
+ jsonConfigPath,
+ expect.not.stringContaining("export default")
+ );
- runInit();
+ expect(mockConsole).toHaveBeenCalledWith(
+ colorsMap.info,
+ "- A codefather.json config file has been created."
+ );
+ });
+
+ test("does not create codefather.ts if it already exists", async () => {
+ (fs.existsSync as jest.Mock).mockImplementation(
+ (file) => file === configPath
+ );
+
+ await runInit();
expect(fs.writeFileSync).not.toHaveBeenCalledWith(
configPath,
@@ -62,15 +189,30 @@ describe("codefather init script", () => {
);
});
- test("adds script to package.json if missing", () => {
+ test("overwrites an existing codefather config the overwrite flag is provided", async () => {
+ process.argv = ["node", "init.js", "overwrite"];
+ (fs.existsSync as jest.Mock).mockImplementation(() => true);
+ (fs.readFileSync as jest.Mock).mockReturnValue(
+ JSON.stringify({ scripts: {} }, null, 2)
+ );
+
+ await runInit();
+
+ expect(mockConsole).toHaveBeenCalledWith(
+ colorsMap.info,
+ "- A new codefather.ts config file has been created, your former config was overwritten."
+ );
+ });
+
+ test("adds script to package.json if missing", async () => {
(fs.existsSync as jest.Mock).mockImplementation(
- (p: string) => p !== configPath
+ (file: string) => file === pkgPath
);
(fs.readFileSync as jest.Mock).mockReturnValue(
JSON.stringify({ scripts: {} }, null, 2)
);
- runInit();
+ await runInit();
expect(fs.writeFileSync).toHaveBeenCalledWith(
pkgPath,
@@ -82,13 +224,15 @@ describe("codefather init script", () => {
);
});
- test("skips adding script if already present", () => {
- (fs.existsSync as jest.Mock).mockImplementation(() => true);
+ test("skips adding script if already present", async () => {
+ (fs.existsSync as jest.Mock).mockImplementation(
+ (file) => file !== codeownersPath
+ );
(fs.readFileSync as jest.Mock).mockReturnValue(
JSON.stringify({ scripts: { codefather: "codefather" } }, null, 2)
);
- runInit();
+ await runInit();
expect(fs.writeFileSync).not.toHaveBeenCalledWith(
pkgPath,
@@ -100,14 +244,14 @@ describe("codefather init script", () => {
);
});
- test("return an error if package.json is missing", () => {
+ test("return an error if package.json is missing", async () => {
(fs.existsSync as jest.Mock).mockImplementation(() => false);
- runInit();
+ await runInit();
expect(mockConsole).toHaveBeenCalledWith(
colorsMap.error,
- "⚠️ No package.json found in the project root. Skipping script setup."
+ "⚠ No package.json found in the project root. Skipping script setup."
);
expect(mockConsole).not.toHaveBeenCalledWith(
colorsMap.success,
@@ -115,10 +259,27 @@ describe("codefather init script", () => {
);
});
- test("logs final setup confirmation", () => {
- (fs.existsSync as jest.Mock).mockImplementation(() => true);
+ it("return an error if the codeowner file is invalid", async () => {
+ (fs.existsSync as jest.Mock).mockImplementation(
+ (file) => file === codeownersPath
+ );
+ (generateConfigFromCodeowners as jest.Mock).mockImplementationOnce(() => {
+ throw new Error("𐄂 Your CODEOWNER file is invalid.");
+ });
+ await runInit();
+ expect(mockConsole).toHaveBeenCalledWith(
+ colorsMap.error,
+ "𐄂 Your CODEOWNER file is invalid."
+ );
+ });
- runInit();
+ test("logs final setup confirmation", async () => {
+ (fs.existsSync as jest.Mock).mockImplementation(() => true);
+ (generateConfigFromCodeowners as jest.Mock).mockImplementation(() => ({
+ config: { rules: [] },
+ crews: [],
+ }));
+ await runInit();
expect(mockConsole).toHaveBeenCalledWith(
colorsMap.success,
diff --git a/shared/file-validator/file-validator.test.ts b/shared/file-validator/file-validator.test.ts
index 8c70b0d..1a1ccec 100644
--- a/shared/file-validator/file-validator.test.ts
+++ b/shared/file-validator/file-validator.test.ts
@@ -16,14 +16,14 @@ describe("validateFiles", () => {
rules: [
{
match: ["src/core/**"],
- goodfellas: [{ emailPrefix: "johnny.fontane" }],
+ goodfellas: [{ name: "oldblueeyes" }],
},
],
};
expect(
validateFiles(
[resolve(cwd, "src/core/index.ts")],
- [{ name: "oldblueeyes", emailPrefix: "johnny.fontane" }],
+ [{ name: "oldblueeyes" }],
config
)
).toEqual({ errors: [], warnings: [] });
@@ -34,14 +34,14 @@ describe("validateFiles", () => {
rules: [
{
match: ["src/core/**"],
- goodfellas: [{ name: "oldblueeyes" }, { name: "@tomhagen" }],
+ goodfellas: [{ name: "oldblueeyes" }, { name: "tomhagen" }],
},
],
};
expect(
validateFiles(
[resolve(cwd, "src/core/index.ts")],
- [{ name: "oldblueeyes" }, { name: "@tomhagen" }],
+ [{ name: "oldblueeyes" }, { name: "tomhagen" }],
config
)
).toEqual({ errors: [], warnings: [] });
@@ -53,7 +53,7 @@ describe("validateFiles", () => {
rules: [
{
match: ["src/core/**"],
- goodfellas: [{ name: "oldblueeyes", emailPrefix: "johnny.fontane" }],
+ goodfellas: [{ name: "oldblueeyes" }],
},
],
};
@@ -91,18 +91,18 @@ describe("validateFiles", () => {
rules: [
{
match: ["src/core/**"],
- goodfellas: [{ name: "oldblueeyes", emailPrefix: "johnny.fontane" }],
+ goodfellas: [{ name: "oldblueeyes" }],
crews: ["clemenzaPeople"],
},
],
crews: {
- clemenzaPeople: [{ name: "@paulieGatto" }, { name: "@lucabrasi" }],
+ clemenzaPeople: [{ name: "paulieGatto" }, { name: "lucabrasi" }],
},
};
expect(
validateFiles(
[resolve(cwd, "src/core/index.ts")],
- [{ name: "@paulieGatto" }],
+ [{ name: "paulieGatto" }],
config
)
).toEqual({ errors: [], warnings: [] });
@@ -115,14 +115,14 @@ describe("validateFiles", () => {
rules: [
{
match: ["src/core/**"],
- goodfellas: [{ emailPrefix: "johnny.fontane" }],
+ goodfellas: [{ name: "oldblueeyes" }],
},
],
};
const result = validateFiles(
[resolve(cwd, "src/core/index.ts")],
- [{ name: "@tomhagen" }],
+ [{ name: "tomhagen" }],
config
);
@@ -144,7 +144,7 @@ describe("validateFiles", () => {
};
const result = validateFiles(
[resolve(cwd, "src/core/index.ts")],
- [{ name: "oldblueeyes" }, { name: "@tomhagen" }],
+ [{ name: "oldblueeyes" }, { name: "tomhagen" }],
config
);
expect(result.errors.length).toBe(1);
@@ -167,7 +167,7 @@ describe("validateFiles", () => {
const result = validateFiles(
[resolve(cwd, "src/core/index.ts")],
- [{ name: "@tomhagen" }, { name: "solozzo" }],
+ [{ name: "tomhagen" }, { name: "solozzo" }],
config
);
expect(result.errors.length).toBe(1);
@@ -182,7 +182,7 @@ describe("validateFiles", () => {
rules: [
{
match: ["src/core/**"],
- goodfellas: [{ emailPrefix: "johnny.fontane" }],
+ goodfellas: [{ name: "oldblueeyes" }],
allowForgiveness: true,
},
],
@@ -190,7 +190,7 @@ describe("validateFiles", () => {
const result = validateFiles(
[resolve(cwd, "src/core/index.ts")],
- [{ name: "@tomhagen" }],
+ [{ name: "tomhagen" }],
config
);
expect(result.errors.length).toBe(0);
@@ -213,7 +213,7 @@ describe("validateFiles", () => {
const result = validateFiles(
[resolve(cwd, "src/core/index.ts")],
- [{ name: "@tomhagen" }, { name: "mike" }],
+ [{ name: "tomhagen" }, { name: "mike" }],
config
);
expect(result.errors.length).toBe(0);
@@ -228,20 +228,20 @@ describe("validateFiles", () => {
rules: [
{
match: ["src/core/**"],
- goodfellas: [{ emailPrefix: "johnny.fontane" }],
+ goodfellas: [{ name: "oldblueeyes" }],
allowForgiveness: true,
crews: ["tessioTeam"],
},
],
crews: {
- clemenzaPeople: [{ name: "@paulieGatto" }, { name: "@lucabrasi" }],
+ clemenzaPeople: [{ name: "paulieGatto" }, { name: "lucabrasi" }],
tessioTeam: [{ name: "salvatore" }],
},
};
const result = validateFiles(
[resolve(cwd, "src/core/index.ts")],
- [{ name: "@paulieGatto" }],
+ [{ name: "paulieGatto" }],
config
);
expect(result.errors.length).toBe(0);
@@ -254,7 +254,7 @@ describe("validateFiles", () => {
rules: [
{
match: [/index\.ts$/],
- goodfellas: [{ emailPrefix: "johnny.fontane" }],
+ goodfellas: [{ name: "oldblueeyes" }],
message: "Don't touch that!",
},
],
@@ -262,7 +262,7 @@ describe("validateFiles", () => {
const result = validateFiles(
[resolve(cwd, "weird/nested/path/index.ts")],
- [{ name: "@tomhagen" }],
+ [{ name: "tomhagen" }],
config
);
expect(result.errors.length).toBe(1);
@@ -274,12 +274,12 @@ describe("validateFiles", () => {
rules: [
{
match: ["src/core/**"],
- goodfellas: [{ emailPrefix: "johnny.fontane" }],
+ goodfellas: [{ name: "oldblueeyes" }],
message: "Don't touch that!",
},
{
match: ["*.env", "config/**", "src/utils/helpers.ts"],
- goodfellas: [{ emailPrefix: "tom.woltz" }],
+ goodfellas: [{ name: "tom.woltz" }],
message: "Don't even think about it.",
},
],
@@ -292,7 +292,7 @@ describe("validateFiles", () => {
resolve(cwd, "src/utils/helpers.ts"),
];
- const result = validateFiles(files, [{ emailPrefix: "tom.hagen" }], config);
+ const result = validateFiles(files, [{ name: "tomhagen" }], config);
expect(result.errors).toHaveLength(2); // 3 unauthorized changes are spotted, but 2 rules are broken
expect(result.errors[0]).toContain("Don't touch that!");
expect(result.errors[1]).toContain("Don't even think about it.");
@@ -304,7 +304,7 @@ describe("validateFiles", () => {
expect(
validateFiles(
[resolve(cwd, "src/core/index.ts")],
- [{ emailPrefix: "tom.hagen" }],
+ [{ name: "tomhagen" }],
config
)
).toStrictEqual({ errors: [], warnings: [] });
@@ -327,7 +327,7 @@ describe("validateFiles", () => {
resolve(cwd, "project/src/models/index.ts"),
resolve(cwd, "project/.env"),
],
- [{ name: "@tomhagen" }],
+ [{ name: "tomhagen" }],
config
)
).toStrictEqual({ errors: [], warnings: [] });
diff --git a/shared/file-validator/index.ts b/shared/file-validator/index.ts
index b5dccad..decdc0a 100644
--- a/shared/file-validator/index.ts
+++ b/shared/file-validator/index.ts
@@ -13,12 +13,8 @@ function areCommittersIncluded(
committers: GitUser[]
): boolean {
if (!authorizedUsers?.length) return false;
- return committers.every(({ name, emailPrefix }) =>
- authorizedUsers.some(
- (user) =>
- (name && user.name === name) ||
- (emailPrefix && user.emailPrefix === emailPrefix)
- )
+ return committers.every(({ name }) =>
+ authorizedUsers.some((user) => name && user.name === name)
);
}
@@ -27,12 +23,7 @@ function getUnauthorizedCommitters(
committers: GitUser[]
): GitUser[] {
return committers.filter(
- ({ name, emailPrefix }) =>
- !authorizedUsers.some(
- (user) =>
- (name && user.name === name) ||
- (emailPrefix && user.emailPrefix === emailPrefix)
- )
+ ({ name }) => !authorizedUsers.some((user) => name && user.name === name)
);
}
diff --git a/shared/formatter/formatter.test.ts b/shared/formatter/formatter.test.ts
deleted file mode 100644
index 4b7e07c..0000000
--- a/shared/formatter/formatter.test.ts
+++ /dev/null
@@ -1,14 +0,0 @@
-import { getEmailPrefix } from ".";
-
-describe("getEmailPrefix", () => {
- it("return the prefix of an email", () => {
- expect(getEmailPrefix("kay@corleone.com")).toBe("kay");
- expect(getEmailPrefix("tom.hagen@don.com")).toBe("tom.hagen");
- expect(getEmailPrefix("amerigo-bonasera@nyc.com")).toBe("amerigo-bonasera");
- });
- it("return undefined if there is no email", () => {
- expect(getEmailPrefix("")).toBeUndefined();
- expect(getEmailPrefix(undefined)).toBeUndefined();
- expect(getEmailPrefix(null)).toBeUndefined();
- });
-});
diff --git a/shared/formatter/index.ts b/shared/formatter/index.ts
deleted file mode 100644
index bc03ace..0000000
--- a/shared/formatter/index.ts
+++ /dev/null
@@ -1,6 +0,0 @@
-export function getEmailPrefix(
- email: string | undefined | null
-): string | undefined {
- if (!email) return undefined;
- return email.trim().split("@")[0];
-}
diff --git a/shared/messages/index.ts b/shared/messages/index.ts
index 2d58625..a5e049e 100644
--- a/shared/messages/index.ts
+++ b/shared/messages/index.ts
@@ -142,11 +142,7 @@ const messagesMap: Record = {
function formatUserList(users: GitUser[], type: Intl.ListFormatType): string {
const usernames = [
- ...new Set(
- users
- .map(({ name, emailPrefix }) => (name || emailPrefix)?.trim())
- .filter(Boolean)
- ),
+ ...new Set(users.map(({ name }) => name?.trim()).filter(Boolean)),
] as string[];
return new Intl.ListFormat("en", {
diff --git a/shared/messages/messages.test.ts b/shared/messages/messages.test.ts
index 372d83c..c4f89bd 100644
--- a/shared/messages/messages.test.ts
+++ b/shared/messages/messages.test.ts
@@ -9,20 +9,17 @@ describe("getRandomMessage", () => {
describe("Success MessageType", () => {
it("returns a success message with a committer", () => {
const msg = getRandomMessage(MessageType.Success, {
- committers: [{ name: "oldblueeyes", emailPrefix: "johnny.fontane" }],
+ committers: [{ name: "oldblueeyes" }],
});
expect(msg).toBe("✓ Thank you oldblueeyes. You respected the codebase.");
});
it("returns a success message with committers", () => {
const msg = getRandomMessage(MessageType.Success, {
- committers: [
- { name: "oldblueeyes", emailPrefix: "johnny.fontane" },
- { name: "@tomhagen", emailPrefix: "tom.hagen" },
- ],
+ committers: [{ name: "oldblueeyes" }, { name: "tomhagen" }],
});
expect(msg).toBe(
- "✓ Thank you oldblueeyes and @tomhagen. You respected the codebase."
+ "✓ Thank you oldblueeyes and tomhagen. You respected the codebase."
);
});
@@ -33,8 +30,8 @@ describe("getRandomMessage", () => {
it("doesn't inject goodfellas in the message", () => {
const msg = getRandomMessage(MessageType.Success, {
- goodfellas: [{ name: "solozzo" }, { name: "@lucabrasi" }],
- committers: [{ name: "oldblueeyes", emailPrefix: "johnny.fontane" }],
+ goodfellas: [{ name: "solozzo" }, { name: "lucabrasi" }],
+ committers: [{ name: "oldblueeyes" }],
});
expect(msg).toBe("✓ Thank you oldblueeyes. You respected the codebase.");
});
@@ -54,10 +51,10 @@ describe("getRandomMessage", () => {
it("returns an error message with committers and one goodfella", () => {
const msg = getRandomMessage(MessageType.Error, {
goodfellas: [{ name: "solozzo" }],
- committers: [{ name: "sonny" }, { name: "@tomhagen" }],
+ committers: [{ name: "sonny" }, { name: "tomhagen" }],
});
expect(msg).toBe(
- "𐄂 sonny and @tomhagen! You need permission from my trusted associate: solozzo. Nobody touches this without approval."
+ "𐄂 sonny and tomhagen! You need permission from my trusted associate: solozzo. Nobody touches this without approval."
);
});
@@ -66,12 +63,12 @@ describe("getRandomMessage", () => {
goodfellas: [
{ name: "solozzo" },
{ name: "mike" },
- { emailPrefix: "johnny.fontane" },
+ { name: "johnny.fontane" },
],
- committers: [{ name: "sonny" }, { name: "@tomhagen" }],
+ committers: [{ name: "sonny" }, { name: "tomhagen" }],
});
expect(msg).toBe(
- "𐄂 sonny and @tomhagen! You need permission from my trusted associates: solozzo, mike, or johnny.fontane. Nobody touches this without approval."
+ "𐄂 sonny and tomhagen! You need permission from my trusted associates: solozzo, mike, or johnny.fontane. Nobody touches this without approval."
);
});
@@ -80,7 +77,7 @@ describe("getRandomMessage", () => {
goodfellas: [
{ name: "solozzo" },
{ name: "mike" },
- { emailPrefix: "johnny.fontane" },
+ { name: "johnny.fontane" },
],
committers: [{ name: "sonny" }],
});
@@ -101,10 +98,10 @@ describe("getRandomMessage", () => {
it("returns a warning message with one committer and one goodfella", () => {
const msg = getRandomMessage(MessageType.Warning, {
committers: [{ name: "solozzo" }],
- goodfellas: [{ name: "@tomhagen" }],
+ goodfellas: [{ name: "tomhagen" }],
});
expect(msg).toBe(
- "⚠ solozzo: You ain't made, but we’ll let it slide this time. Get @tomhagen to vouch for ya."
+ "⚠ solozzo: You ain't made, but we’ll let it slide this time. Get tomhagen to vouch for ya."
);
});
@@ -115,10 +112,10 @@ describe("getRandomMessage", () => {
{ name: "oldblueeyes" },
{ name: "mike" },
],
- goodfellas: [{ name: "@tomhagen" }],
+ goodfellas: [{ name: "tomhagen" }],
});
expect(msg).toBe(
- "⚠ solozzo, oldblueeyes, and mike: You ain't made, but we’ll let it slide this time. Get @tomhagen to vouch for ya."
+ "⚠ solozzo, oldblueeyes, and mike: You ain't made, but we’ll let it slide this time. Get tomhagen to vouch for ya."
);
});
@@ -129,10 +126,10 @@ describe("getRandomMessage", () => {
{ name: "oldblueeyes" },
{ name: "mike" },
],
- goodfellas: [{ name: "@tomhagen" }, { name: "@lucabrasi" }],
+ goodfellas: [{ name: "tomhagen" }, { name: "lucabrasi" }],
});
expect(msg).toBe(
- "⚠ solozzo, oldblueeyes, and mike: You ain't made, but we’ll let it slide this time. Get @tomhagen or @lucabrasi to vouch for ya."
+ "⚠ solozzo, oldblueeyes, and mike: You ain't made, but we’ll let it slide this time. Get tomhagen or lucabrasi to vouch for ya."
);
});
@@ -147,10 +144,10 @@ describe("getRandomMessage", () => {
it("returns a warning message with no committer and one goodfellas", () => {
const msg = getRandomMessage(MessageType.Warning, {
- goodfellas: [{ name: "@lucabrasi" }],
+ goodfellas: [{ name: "lucabrasi" }],
});
expect(msg).toBe(
- "⚠ Committer: You ain't made, but we’ll let it slide this time. Get @lucabrasi to vouch for ya."
+ "⚠ Committer: You ain't made, but we’ll let it slide this time. Get lucabrasi to vouch for ya."
);
});
@@ -190,7 +187,7 @@ describe("getRandomMessage", () => {
it("doesn't inject goodfella or committers", () => {
const msg = getRandomMessage(MessageType.NotFound, {
- goodfellas: [{ name: "oldblueeyes" }, { name: "@lucabrasi" }],
+ goodfellas: [{ name: "oldblueeyes" }, { name: "lucabrasi" }],
committers: [{ name: "mile" }],
});
expect(msg).toBe(
@@ -207,7 +204,7 @@ describe("getRandomMessage", () => {
it("doesn't inject goodfella or committers", () => {
const msg = getRandomMessage(MessageType.NoGitConfig, {
- goodfellas: [{ name: "oldblueeyes" }, { name: "@lucabrasi" }],
+ goodfellas: [{ name: "oldblueeyes" }, { name: "lucabrasi" }],
committers: [{ name: "mile" }],
});
expect(msg).toBe("𐄂 You don't have a git config... Are you a cop?");
@@ -235,17 +232,17 @@ describe("getRandomMessage", () => {
{ name: "solozzo" },
{ name: "solozzo" },
{ name: "oldblueeyes" },
- { emailPrefix: "michael.corleone" },
- { emailPrefix: "michael.corleone" },
+ { name: "michael.corleone" },
+ { name: "michael.corleone" },
],
goodfellas: [
- { name: "@tomhagen" },
- { name: "@lucabrasi" },
- { name: "@tomhagen" },
+ { name: "tomhagen" },
+ { name: "lucabrasi" },
+ { name: "tomhagen" },
],
});
expect(msg).toBe(
- "⚠ solozzo, oldblueeyes, and michael.corleone: You ain't made, but we’ll let it slide this time. Get @tomhagen or @lucabrasi to vouch for ya."
+ "⚠ solozzo, oldblueeyes, and michael.corleone: You ain't made, but we’ll let it slide this time. Get tomhagen or lucabrasi to vouch for ya."
);
});
});
diff --git a/shared/models/index.ts b/shared/models/index.ts
index 6eec4d0..8a92dd6 100644
--- a/shared/models/index.ts
+++ b/shared/models/index.ts
@@ -1,6 +1,5 @@
export type GitUser = {
name?: string;
- emailPrefix?: string;
};
export type CrewName = string;
@@ -8,12 +7,9 @@ export type CrewName = string;
export interface CodefatherRule {
/** List of the files or folders that can only be modified by a given list of users */
match: Array;
- /** List of users authorized to modify the list of files or folders.
- * name: github username.
- * emailPrefix: prefix of the user email tied to their Github account (e.g. johnny.fontane@jazz.com should be johnny.fontane) .
- */
+ /** List of users authorized to modify the list of files or folders. */
goodfellas: GitUser[];
- /** List of authorized user crews. The crews must be defined at the root of your config when used in CLI mode. */
+ /** List of authorized user crews (teams). The crews must be defined at the root of your config when used in CLI mode. */
crews?: CrewName[];
/** The message displayed if an unauthorized user tries to modify a protected file. If empty, a random message will be generated. */
message?: string;
@@ -22,10 +18,7 @@ export interface CodefatherRule {
}
export interface CodefatherConfig {
- /** List of users authorized to modify any files in your repository.
- * name: github username.
- * emailPrefix: prefix of the user email tied to their Github account (e.g. johnny.fontane@jazz.com should be johnny.fontane) .
- */
+ /** List of users authorized to modify any files in your repository. */
caporegimes?: GitUser[];
/** Rules that apply to protected files and folders */
rules: CodefatherRule[];
@@ -33,17 +26,17 @@ export interface CodefatherConfig {
options?: {
/** If true, the codefather face will appear in the terminal. Defaults to true. */
showAscii?: boolean;
- /** If true, all the pull request committers will be checked against the authorized users. Only used in a github action context. Defaults to true. */
+ /** If true, all the pull request committers will be checked against the authorized users. Only used in a Github Action context. Defaults to true. */
vouchForAllCommitters?: boolean;
};
/** Options to auto assign reviewers on Github */
codeReviews?: {
/** If true, goodfellas responsible for modified files will be assigned on relevant pull requests, except the committers. Defaults to true. */
- autoAssignGoodfellas: boolean;
- /** If true, caporegimes will be assigned on every pull request except the committers. Defaults to false. */
- autoAssignCaporegimes: boolean;
+ autoAssignGoodfellas?: boolean;
+ /** If true, caporegimes will be assigned on every pull request, except the committers. Defaults to false. */
+ autoAssignCaporegimes?: boolean;
};
- /** Group users in teams. The crews names and composition are free in cli mode, but should match your github teams if used in a github action */
+ /** Group users in teams. Crew names and composition are flexible in CLI mode but should match your github teams if used in a Github Action */
crews?: Record;
}
diff --git a/shared/parser/parser.test.ts b/shared/parser/parser.test.ts
index ff68115..531ab63 100644
--- a/shared/parser/parser.test.ts
+++ b/shared/parser/parser.test.ts
@@ -6,12 +6,12 @@ const config: CodefatherConfig = {
rules: [
{
match: ["src/core/**"],
- goodfellas: [{ name: "oldblueeyes", emailPrefix: "johnny.fontane" }],
+ goodfellas: [{ name: "oldblueeyes" }],
crews: ["clemenzaPeople"],
},
],
crews: {
- clemenzaPeople: [{ name: "@paulieGatto" }, { name: "@lucabrasi" }],
+ clemenzaPeople: [{ name: "paulieGatto" }, { name: "lucabrasi" }],
},
};