Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions src/action/isEntityAssociationIncluded.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { Entity } from "../types/entities";

/**
* Determines if an entity should be included based on its author association.
* Uses the author_association field from GitHub's webhook payload.
* @param entity The entity to check
* @param includeAssociations Set of allowed author associations
* @returns true if the entity should be included, false if it should be skipped
*/
export function isEntityAssociationIncluded(
entity: Entity,
includeAssociations: Set<string>,
) {
if ("author_association" in entity.data) {
const association = entity.data.author_association;
return includeAssociations.has(association);
}

return true;
}
15 changes: 15 additions & 0 deletions src/action/isEntityFromBot.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { Entity } from "../types/entities";

/**
* Determines if an entity was created by a bot based on the user.type field.
* @param entity The entity to check
* @returns true if the entity was created by a bot (user.type === "Bot"), false otherwise
*/
export function isEntityFromBot(entity: Entity) {
return (
"user" in entity.data &&
!!entity.data.user &&
"type" in entity.data.user &&
entity.data.user.type === "Bot"
);
}
107 changes: 35 additions & 72 deletions src/action/runOctoGuideAction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
import * as core from "@actions/core";

import { parseCommentId, parseEntityUrl } from "../actors/parseEntity.js";
import { parseIncludeAssociations } from "../execution/parseIncludeAssociations.js";
import { parseRulesConfig } from "../execution/parseRulesConfig.js";
import { runOctoGuideRules } from "../index.js";
import { cliReporter } from "../reporters/cliReporter.js";
import { allRules } from "../rules/all.js";
Expand Down Expand Up @@ -75,6 +77,12 @@
}, {});

const settings = {
baseOptions: {
includeAssociations: parseIncludeAssociations(
core.getInput("include-associations"),
),
includeBots: core.getInput("include-bots") === "true",
},
comments: {
footer:
core.getInput("comment-footer") ||
Expand Down Expand Up @@ -124,66 +132,33 @@
entityType,
);

/**
* Determines if an entity was created by a bot based on the user.type field.
* @param entity The entity to check
* @returns true if the entity was created by a bot (user.type === "Bot"), false otherwise
*/
const isEntityFromBot = (entity: Entity): boolean => {
return (
"user" in entity.data &&
!!entity.data.user &&
"type" in entity.data.user &&
entity.data.user.type === "Bot"
);
};

const includeBots = core.getInput("include-bots") === "true";
if (!includeBots && isEntityFromBot(entityInput)) {
core.info(`Skipping OctoGuide rules for bot-created ${entityType}: ${url}`);
return;
}

/**
* Determines if an entity should be included based on its author association.
* Uses the author_association field from GitHub's webhook payload.
* @param entity The entity to check
* @param includeAssociations Set of allowed author associations
* @returns true if the entity should be included, false if it should be skipped
*/
const shouldIncludeEntity = (
entity: Entity,
includeAssociations: Set<string>,
): boolean => {
if ("author_association" in entity.data) {
const association = entity.data.author_association;
return includeAssociations.has(association);
}

return true;
};

const includeAssociationsInput = core.getInput("include-associations");

const includeAssociations = new Set(
includeAssociationsInput
.split(",")
.map((a) => a.trim())
.filter((a) => a.length > 0),
);

includeAssociations.add("NONE");

if (!shouldIncludeEntity(entityInput, includeAssociations)) {
const association =
"author_association" in entityInput.data
? entityInput.data.author_association
: "UNKNOWN";
core.info(
`Skipping OctoGuide rules for ${association} created ${entityType}: ${url}`,
);
return;
}
// TODO: can we still get fast-fail?
// what if the user hasn't provided any options?

// // TODO: move down to rule-level

// if (!settings.baseOptions.includeBots && isEntityFromBot(entityInput)) {
// core.info(`Skipping OctoGuide rules for bot-created ${entityType}: ${url}`);
// return;
// }

// // TODO: move associations down to rule-level

// if (
// !isEntityAssociationIncluded(
// entityInput,
// settings.baseOptions.includeAssociations,
// )
// ) {
// const association =
// "author_association" in entityInput.data
// ? entityInput.data.author_association
// : "UNKNOWN";
// core.info(
// `Skipping OctoGuide rules for ${association} created ${entityType}: ${url}`,
// );
// return;
// }

const { actor, entity, reports } = await runOctoGuideRules({
auth,
Expand All @@ -191,7 +166,7 @@
settings,
});

if (reports.length) {

Check failure on line 169 in src/action/runOctoGuideAction.ts

View workflow job for this annotation

GitHub Actions / Test

src/action/runOctoGuideAction.test.ts > runOctoGuideAction > include-bots configuration > user is a bot > should skip rule execution when include-bots defaults to false

TypeError: Cannot read properties of undefined (reading 'length') ❯ Module.runOctoGuideAction src/action/runOctoGuideAction.ts:169:14 ❯ src/action/runOctoGuideAction.test.ts:979:5
core.info(`Found ${reports.length} report(s).`);
console.log(cliReporter(reports));
} else {
Expand Down Expand Up @@ -430,15 +405,3 @@
typeof data.html_url === "string"
);
}

function parseRulesConfig(input: string) {
if (input === "") {
return {};
}

try {
return JSON.parse(input) as Record<string, boolean | undefined>;
} catch (error) {
return error as Error;
}
}
21 changes: 21 additions & 0 deletions src/execution/isRuleSkippedForEntity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { isEntityFromBot } from "../action/isEntityFromBot";
import { Entity } from "../types/entities";
import { Rule, RuleOptions } from "../types/rules";

export function isRuleSkippedForEntity(
entity: Entity,
options: RuleOptions,
rule: Rule,
) {
const includeAssociations = options["include-associations"]?.split(",");
if (!includeAssociations) {
// return false;
}

const includeBots = options["include-bots"];
if (isEntityFromBot(entity) && !includeBots) {
return true;
}

return true;
}
23 changes: 23 additions & 0 deletions src/execution/mergeRuleOptions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { RuleOptions, RuleOptionsRaw } from "../types/rules";
import { BaseOptions } from "../types/settings";

export function mergeRuleOptions(
baseOptions: BaseOptions,
overrides: boolean | RuleOptionsRaw | undefined,
): RuleOptions {
if (!overrides || typeof overrides === "boolean") {
return {
includeAssociations: baseOptions.includeAssociations,

Check failure on line 10 in src/execution/mergeRuleOptions.ts

View workflow job for this annotation

GitHub Actions / Test

src/runOctoGuideRules.test.ts > runOctoGuideRules > should use pre-fetched entity data instead of URL string when entity object is provided

TypeError: Cannot read properties of undefined (reading 'includeAssociations') ❯ mergeRuleOptions src/execution/mergeRuleOptions.ts:10:37 ❯ src/runOctoGuideRules.ts:126:20 ❯ Module.runOctoGuideRules src/runOctoGuideRules.ts:124:16 ❯ src/runOctoGuideRules.test.ts:469:18

Check failure on line 10 in src/execution/mergeRuleOptions.ts

View workflow job for this annotation

GitHub Actions / Test

src/runOctoGuideRules.test.ts > runOctoGuideRules > should filter rules when rule overrides are provided

TypeError: Cannot read properties of undefined (reading 'includeAssociations') ❯ mergeRuleOptions src/execution/mergeRuleOptions.ts:10:37 ❯ src/runOctoGuideRules.ts:126:20 ❯ Module.runOctoGuideRules src/runOctoGuideRules.ts:124:16 ❯ src/runOctoGuideRules.test.ts:403:18

Check failure on line 10 in src/execution/mergeRuleOptions.ts

View workflow job for this annotation

GitHub Actions / Test

src/runOctoGuideRules.test.ts > runOctoGuideRules > should collect reports when rules call the report function

TypeError: Cannot read properties of undefined (reading 'includeAssociations') ❯ mergeRuleOptions src/execution/mergeRuleOptions.ts:10:37 ❯ src/runOctoGuideRules.ts:126:20 ❯ Module.runOctoGuideRules src/runOctoGuideRules.ts:124:16 ❯ src/runOctoGuideRules.test.ts:321:18

Check failure on line 10 in src/execution/mergeRuleOptions.ts

View workflow job for this annotation

GitHub Actions / Test

src/runOctoGuideRules.test.ts > runOctoGuideRules > should run all config rules when no rule settings are provided

TypeError: Cannot read properties of undefined (reading 'includeAssociations') ❯ mergeRuleOptions src/execution/mergeRuleOptions.ts:10:37 ❯ src/runOctoGuideRules.ts:126:20 ❯ Module.runOctoGuideRules src/runOctoGuideRules.ts:124:16 ❯ src/runOctoGuideRules.test.ts:266:18
includeBots: baseOptions.includeBots,
};
}

return {
...baseOptions,
...overrides,
"include-associations": overrides["include-associations"]
? new Set(overrides["include-associations"])
: baseOptions.includeAssociations,
"include-bots": overrides["include-bots"] ?? baseOptions.includeBots,
};
}
20 changes: 20 additions & 0 deletions src/execution/parseIncludeAssociations.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
const defaultIncludeAssociations = new Set([
"CONTRIBUTOR",
"FIRST_TIME_CONTRIBUTOR",
"FIRST_TIMER",
"NONE",
]);

export function parseIncludeAssociations(raw: string) {
if (!raw) {
return defaultIncludeAssociations;
}

return new Set([
"NONE",
...raw
.split(",")
.map((a) => a.trim())
.filter((a) => a.length > 0),
]);
}
27 changes: 27 additions & 0 deletions src/execution/parseRulesConfig.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { RuleOptions, RuleOptionsRaw } from "../types/rules";
import { parseIncludeAssociations } from "./parseIncludeAssociations";

export function parseRulesConfig(input: string) {
if (input === "") {
return {};
}

try {
return processRulesConfig(JSON.parse(input) as RuleOptionsRaw);
} catch (error) {
return error as Error;
}
}

function processRulesConfig(raw: RuleOptionsRaw): RuleOptions {
return {
...raw,
// TODO: this is wrong
// these shouldn't be at root
// these are for each rule
"include-associations": raw["include-associations"]
? parseIncludeAssociations(raw["include-associations"])
: undefined,
"include-bots": !raw["include-bots"],
};
}
19 changes: 15 additions & 4 deletions src/runOctoGuideRules.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
import type { Settings } from "./types/settings.js";

import { createActor } from "./actors/createActor.js";
import { isRuleSkippedForEntity } from "./execution/isRuleSkippedForEntity.js";
import { mergeRuleOptions } from "./execution/mergeRuleOptions.js";
import { runRuleOnEntity } from "./execution/runRuleOnEntity.js";
import { allRules } from "./rules/all.js";
import { configs } from "./rules/configs.js";
Expand All @@ -31,7 +33,7 @@
/**
* Settings for the run, including rules to enable.
*/
settings?: Settings;
settings: Settings;
}

/**
Expand Down Expand Up @@ -102,11 +104,11 @@

const reports: RuleReport[] = [];

const config = settings?.config ?? "recommended";
const config = settings.config ?? "recommended";

Check failure on line 107 in src/runOctoGuideRules.ts

View workflow job for this annotation

GitHub Actions / Test

src/runOctoGuideRules.test.ts > runOctoGuideRules > should default to recommended config when no settings are provided

TypeError: Cannot read properties of undefined (reading 'config') ❯ Module.runOctoGuideRules src/runOctoGuideRules.ts:107:26 ❯ src/runOctoGuideRules.test.ts:360:18

Check failure on line 107 in src/runOctoGuideRules.ts

View workflow job for this annotation

GitHub Actions / Test

src/runOctoGuideRules.test.ts > runOctoGuideRules > should use the authentication token when provided

TypeError: Cannot read properties of undefined (reading 'config') ❯ Module.runOctoGuideRules src/runOctoGuideRules.ts:107:26 ❯ src/runOctoGuideRules.test.ts:196:3
const configRuleNames = Object.values(configs[config]).map(
(rule) => rule.about.name,
);
const ruleOverrides = settings?.rules ?? {};
const ruleOverrides = settings.rules ?? {};

const enabledRules = allRules.filter((rule) => {
const ruleName = rule.about.name;
Expand All @@ -120,9 +122,16 @@

await Promise.all(
enabledRules.map(async (rule) => {
// TODO: merge with parent options
const options = mergeRuleOptions(
settings.baseOptions,
ruleOverrides[rule.about.name],
);

const context: RuleContext = {
locator,
octokit,
options: typeof options === "object" ? options : undefined,
report(data) {
reports.push({
about: rule.about,
Expand All @@ -131,7 +140,9 @@
},
};

await runRuleOnEntity(context, rule, entity);
if (!isRuleSkippedForEntity(entity, options, rule)) {
await runRuleOnEntity(context, rule, entity);
}
}),
);

Expand Down
23 changes: 23 additions & 0 deletions src/types/rules.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,29 @@ export interface RuleContext {
* Registers a new violation.
*/
report: RuleReporter;

/**
* Processed options for any rule that may be provided by the user.
*/
options?: RuleOptions;
}

/**
* Options for any rule that as provided by the user.
*/
export interface RuleOptionsRaw {
[i: string]: unknown;
"include-associations"?: string;
"include-bots"?: boolean;
}

/**
* Processed options for any rule that may be provided by the user.
*/
export interface RuleOptions {
[i: string]: unknown;
"include-associations"?: Set<string>;
"include-bots"?: boolean;
}

/**
Expand Down
17 changes: 12 additions & 5 deletions src/types/settings.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,19 @@
import type { ConfigName } from "./core.js";
import type { RuleOptions, RuleOptionsRaw } from "./rules.js";

export interface Settings {
comments?: Comments;
config?: ConfigName;
rules?: Record<string, boolean>;
export interface BaseOptions {
includeAssociations?: Set<string>;
includeBots?: boolean;
}

interface Comments {
export interface Comments {
footer: string;
header: string;
}

export interface Settings {
baseOptions: BaseOptions;
comments?: Comments;
config?: ConfigName;
rules?: Record<string, boolean | RuleOptionsRaw>;
}
Loading