Skip to content

Commit

Permalink
chore: added validator
Browse files Browse the repository at this point in the history
  • Loading branch information
gentlementlegen committed Jun 10, 2024
1 parent 7828da6 commit 5a7df72
Show file tree
Hide file tree
Showing 5 changed files with 130 additions and 6 deletions.
Binary file modified bun.lockb
Binary file not shown.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
"dotenv": "^16.4.4",
"lodash": "4.17.21",
"smee-client": "^2.0.0",
"typebox-validators": "0.3.5",
"yaml": "^2.4.1"
},
"devDependencies": {
Expand Down
3 changes: 3 additions & 0 deletions src/github/types/plugin-configuration.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Type as T } from "@sinclair/typebox";
import { StaticDecode } from "@sinclair/typebox";
import { StandardValidator } from "typebox-validators";
import { githubWebhookEvents } from "./webhook-events";

const pluginNameRegex = new RegExp("^([0-9a-zA-Z-._]+)\\/([0-9a-zA-Z-._]+)(?::([0-9a-zA-Z-._]+))?(?:@([0-9a-zA-Z-._]+(?:\\/[0-9a-zA-Z-._]+)?))?$");
Expand Down Expand Up @@ -73,4 +74,6 @@ export const configSchema = T.Object({
plugins: T.Record(T.Enum(githubWebhookEvents), handlerSchema, { default: {} }),
});

export const configSchemaValidator = new StandardValidator(configSchema);

export type PluginConfiguration = StaticDecode<typeof configSchema>;
43 changes: 37 additions & 6 deletions src/github/utils/config.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { emitterEventNames } from "@octokit/webhooks";
import { Value } from "@sinclair/typebox/value";
import { merge } from "lodash";
import { isArray, mergeWith } from "lodash";
import YAML from "yaml";
import { GitHubContext } from "../github-context";
import { expressionRegex } from "../types/plugin";
import { configSchema, PluginConfiguration } from "../types/plugin-configuration";
import { configSchema, configSchemaValidator, PluginConfiguration } from "../types/plugin-configuration";
import { eventNames } from "../types/webhook-events";

const UBIQUIBOT_CONFIG_FULL_PATH = ".github/.ubiquibot-config.yml";
Expand All @@ -19,7 +20,14 @@ async function getConfigurationFromRepo(context: GitHubContext, repository: stri
);
if (targetRepoConfiguration) {
try {
return Value.Decode(configSchema, Value.Default(configSchema, targetRepoConfiguration));
const configSchemaWithDefaults = Value.Default(configSchema, targetRepoConfiguration) as Readonly<unknown>;
const errors = configSchemaValidator.testReturningErrors(configSchemaWithDefaults);
if (errors !== null) {
for (const error of errors) {
console.error(error);
}
}
return Value.Decode(configSchema, configSchemaWithDefaults);
} catch (error) {
console.error(`Error decoding configuration for ${owner}/${repository}, will ignore.`, error);
return null;
Expand All @@ -28,6 +36,29 @@ async function getConfigurationFromRepo(context: GitHubContext, repository: stri
return null;
}

type UsesType = PluginConfiguration["plugins"]["*"][0]["uses"];

function mergeEventNames(arrays: UsesType[]) {
const mergedMap = new Map<string, unknown>();

arrays.flat().forEach((item) => {
const pluginKey = JSON.stringify(item.plugin);
mergedMap.set(pluginKey, item); // Override the content if the key exists
});

return Array.from(mergedMap.values());
}

function customMerge(objValue: UsesType, srcValue: UsesType, key: string) {
if ((emitterEventNames as readonly string[]).includes(key) && isArray(objValue) && isArray(srcValue)) {
return mergeEventNames([objValue, srcValue]);
}
}

function mergeConfigurations(configuration1: PluginConfiguration, configuration2: PluginConfiguration): PluginConfiguration {
return mergeWith({}, configuration1, configuration2, customMerge);
}

export async function getConfig(context: GitHubContext): Promise<PluginConfiguration> {
const payload = context.payload;
const defaultConfiguration = Value.Decode(configSchema, Value.Default(configSchema, {}));
Expand All @@ -36,16 +67,16 @@ export async function getConfig(context: GitHubContext): Promise<PluginConfigura
return defaultConfiguration;
}

let mergedConfiguration = defaultConfiguration;
let mergedConfiguration: PluginConfiguration = defaultConfiguration;

const configurations = await Promise.all([
getConfigurationFromRepo(context, payload.repository.name, payload.repository.owner.login),
getConfigurationFromRepo(context, UBIQUIBOT_CONFIG_ORG_REPO, payload.repository.owner.login),
getConfigurationFromRepo(context, payload.repository.name, payload.repository.owner.login),
]);

configurations.forEach((configuration) => {
if (configuration) {
mergedConfiguration = merge(mergedConfiguration, configuration);
mergedConfiguration = mergeConfigurations(mergedConfiguration, configuration);
}
});

Expand Down
89 changes: 89 additions & 0 deletions tests/main.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { RestEndpointMethodTypes } from "@octokit/plugin-rest-endpoint-methods";
/* eslint-disable @typescript-eslint/naming-convention */
// @ts-expect-error package name is correct, TypeScript doesn't recognize it
import { afterAll, afterEach, beforeAll, describe, expect, it, jest, mock, spyOn } from "bun:test";
Expand Down Expand Up @@ -178,5 +179,93 @@ incentives:
expect(cfg).toBeTruthy();
expect(cfg.incentives.enabled).toBeFalse();
});
it("Should merge organization and repository configuration", async () => {
const cfg = await getConfig({
key: issueOpened,
name: issueOpened,
id: "",
payload: {
repository: {
owner: { login: "ubiquity" },
name: "conversation-rewards",
},
} as unknown as GitHubContext<"issues.closed">["payload"],
octokit: {
rest: {
repos: {
getContent(args: RestEndpointMethodTypes["repos"]["getContent"]["parameters"]) {
if (args.repo !== "ubiquibot-config") {
return {
data: `
plugins:
'*':
- uses:
- plugin: repo-3/plugin-3
type: github
with:
setting1: false
- uses:
- plugin: repo-1/plugin-1
type: github
with:
setting2: true`,
};
}
return {
data: `
plugins:
'*':
- uses:
- plugin: repo-1/plugin-1
type: github
with:
setting1: false
- uses:
- plugin: repo-2/plugin-2
type: github
with:
setting2: true`,
};
},
},
},
},
eventHandler: {} as GitHubEventHandler,
} as unknown as GitHubContext);
expect(cfg.plugins["*"]).toEqual([
{
uses: [
{
plugin: {
owner: "repo-3",
repo: "plugin-3",
workflowId: "compute.yml",
},
type: "github",
with: {
setting1: false,
},
},
],
skipBotEvents: true,
},
{
uses: [
{
plugin: {
owner: "repo-1",
repo: "plugin-1",
workflowId: "compute.yml",
},
type: "github",
with: {
setting2: true,
},
},
],
skipBotEvents: true,
},
]);
});
});
});

0 comments on commit 5a7df72

Please sign in to comment.