Skip to content

Commit

Permalink
fix: schema and decode
Browse files Browse the repository at this point in the history
  • Loading branch information
whilefoo committed Nov 5, 2023
1 parent 3e939d5 commit 4926daf
Show file tree
Hide file tree
Showing 4 changed files with 38 additions and 51 deletions.
15 changes: 9 additions & 6 deletions src/bindings/event.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import Runtime from "./bot-runtime";
import { loadConfiguration } from "./config";
import { Context } from "../types/context";

const allowedEvents = Object.values(GitHubEvent) as string[];

const NO_VALIDATION = [GitHubEvent.INSTALLATION_ADDED_EVENT, GitHubEvent.PUSH_EVENT] as string[];
type PreHandlerWithType = { type: string; actions: PreActionHandler[] };
type HandlerWithType = { type: string; actions: MainActionHandler[] };
Expand All @@ -20,6 +22,8 @@ type PostHandlerWithType = { type: string; actions: PostActionHandler[] };
type AllHandlersWithTypes = PreHandlerWithType | HandlerWithType | PostHandlerWithType;
type AllHandlers = PreActionHandler | MainActionHandler | PostActionHandler;

const validatePayload = ajv.compile(PayloadSchema);

export async function bindEvents(eventContext: ProbotContext) {
const runtime = Runtime.getState();

Expand All @@ -37,8 +41,8 @@ export async function bindEvents(eventContext: ProbotContext) {
}

const payload = eventContext.payload as Payload;
const allowedEvents = Object.values(GitHubEvent) as string[];
const eventName = payload?.action ? `${eventContext.name}.${payload?.action}` : eventContext.name; // some events wont have actions as this grows

if (eventName === GitHubEvent.PUSH_EVENT) {
await validateConfigChange(context);
}
Expand All @@ -47,7 +51,7 @@ export async function bindEvents(eventContext: ProbotContext) {
throw new Error("Failed to create logger");
}

runtime.logger.info("Binding events", { id: eventContext.id, name: eventName, allowedEvents });
runtime.logger.info("Event received", { id: eventContext.id, name: eventName });

if (!allowedEvents.includes(eventName)) {
// just check if its on the watch list
Expand All @@ -58,12 +62,11 @@ export async function bindEvents(eventContext: ProbotContext) {
if (!NO_VALIDATION.includes(eventName)) {
// Validate payload
// console.trace({ payload });
const validate = ajv.compile(PayloadSchema);
const valid = validate(payload);
const valid = validatePayload(payload);
if (!valid) {
// runtime.logger.info("Payload schema validation failed!", payload);
if (validate.errors) {
return runtime.logger.error("validation errors", validate.errors);
if (validatePayload.errors) {
return runtime.logger.error("validation errors", validatePayload.errors);
}
// return;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export async function generatePermit2Signature(
payments: { evmNetworkId },
keys: { evmPrivateEncrypted },
} = context.config;
if (!evmPrivateEncrypted) throw runtime.logger.warn("No bot wallet private key defined");
const { rpc, paymentToken } = getPayoutConfigByNetworkId(evmNetworkId);
const { privateKey } = await decryptKeys(evmPrivateEncrypted);

Expand Down
16 changes: 10 additions & 6 deletions src/types/configuration-types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Type as T, Static, TProperties, TObject, ObjectOptions, StringOptions, StaticDecode } from "@sinclair/typebox";
import { Type as T, Static, TProperties, ObjectOptions, StringOptions, StaticDecode } from "@sinclair/typebox";
import { LogLevel } from "../types";
import { validHTMLElements } from "../handlers/comment/handlers/issue/valid-html-elements";
import { userCommands } from "../handlers";
Expand Down Expand Up @@ -35,13 +35,17 @@ const defaultPriorityLabels = [
{ name: "Priority: 5 (Emergency)" },
];

function StrictObject<T extends TProperties>(obj: T, options?: ObjectOptions): TObject<T> {
function StrictObject<T extends TProperties>(obj: T, options?: ObjectOptions) {
return T.Object<T>(obj, { additionalProperties: false, default: {}, ...options });
}

function stringDuration(options?: StringOptions) {
return T.Transform(T.String(options))
.Decode((value) => {
const decoded = ms(value);
if (decoded === undefined || isNaN(decoded)) {
throw new Error(`Invalid duration string: ${value}`);
}
return ms(value);
})
.Encode((value) => {
Expand All @@ -52,7 +56,7 @@ function stringDuration(options?: StringOptions) {
export const EnvConfigSchema = T.Object({
WEBHOOK_PROXY_URL: T.String({ format: "uri" }),
LOG_ENVIRONMENT: T.String({ default: "production" }),
LOG_LEVEL: T.Enum(LogLevel),
LOG_LEVEL: T.Enum(LogLevel, { default: LogLevel.SILLY }),
LOG_RETRY_LIMIT: T.Number({ default: 8 }),
SUPABASE_URL: T.String({ format: "uri" }),
SUPABASE_KEY: T.String(),
Expand All @@ -67,7 +71,7 @@ export type EnvConfig = Static<typeof EnvConfigSchema>;
export const BotConfigSchema = StrictObject(
{
keys: StrictObject({
evmPrivateEncrypted: T.String(),
evmPrivateEncrypted: T.Optional(T.String()),
openAi: T.Optional(T.String()),
}),
features: StrictObject({
Expand All @@ -89,7 +93,7 @@ export const BotConfigSchema = StrictObject(
}),
timers: StrictObject({
reviewDelayTolerance: stringDuration({ default: "1 day" }),
taskStaleTimeoutDuration: stringDuration({ default: "1 month" }),
taskStaleTimeoutDuration: stringDuration({ default: "4 weeks" }),
taskFollowUpDuration: stringDuration({ default: "0.5 weeks" }),
taskDisqualifyDuration: stringDuration({ default: "1 week" }),
}),
Expand All @@ -108,7 +112,7 @@ export const BotConfigSchema = StrictObject(
),
incentives: StrictObject({
comment: StrictObject({
elements: T.Record(T.Union(HtmlEntities), T.Number(), { default: allHtmlElementsSetToZero }),
elements: T.Record(T.Union(HtmlEntities), T.Number({ default: 0 }), { default: allHtmlElementsSetToZero }),
totals: StrictObject({
character: T.Number({ default: 0, minimum: 0 }),
word: T.Number({ default: 0, minimum: 0 }),
Expand Down
57 changes: 18 additions & 39 deletions src/utils/generate-configuration.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import merge from "lodash/merge";
import { Context } from "probot";
import { Context as ProbotContext } from "probot";
import YAML from "yaml";
import Runtime from "../bindings/bot-runtime";
import { Payload, BotConfig, validateBotConfig, BotConfigSchema } from "../types";
Expand All @@ -9,18 +9,18 @@ import { Value } from "@sinclair/typebox/value";
const UBIQUIBOT_CONFIG_REPOSITORY = "ubiquibot-config";
const UBIQUIBOT_CONFIG_FULL_PATH = ".github/ubiquibot-config.yml";

export async function generateConfiguration(context: Context): Promise<BotConfig> {
export async function generateConfiguration(context: ProbotContext): Promise<BotConfig> {
const payload = context.payload as Payload;

let organizationConfiguration = parseYaml(
const organizationConfiguration = parseYaml(
await download({
context,
repository: UBIQUIBOT_CONFIG_REPOSITORY,
owner: payload.organization?.login || payload.repository.owner.login,
})
);

let repositoryConfiguration = parseYaml(
const repositoryConfiguration = parseYaml(
await download({
context,
repository: payload.repository.name,
Expand All @@ -30,14 +30,9 @@ export async function generateConfiguration(context: Context): Promise<BotConfig

let orgConfig: BotConfig | undefined;
if (organizationConfiguration) {
console.dir(organizationConfiguration, { depth: null, colors: true });
organizationConfiguration = Value.Decode(BotConfigSchema, organizationConfiguration);
const valid = validateBotConfig(organizationConfiguration);
if (!valid) {
const errors = (validateBotConfig.errors as DefinedError[]).filter(
(error) => !(error.keyword === "required" && error.params.missingProperty === "evmPrivateEncrypted")
);
const err = generateValidationError(errors as DefinedError[]);
const err = generateValidationError(validateBotConfig.errors as DefinedError[]);
if (err instanceof Error) throw err;
if (payload.issue?.number)
await context.octokit.issues.createComment({
Expand All @@ -52,14 +47,9 @@ export async function generateConfiguration(context: Context): Promise<BotConfig

let repoConfig: BotConfig | undefined;
if (repositoryConfiguration) {
console.dir(repositoryConfiguration, { depth: null, colors: true });
repositoryConfiguration = Value.Decode(BotConfigSchema, repositoryConfiguration);
const valid = validateBotConfig(repositoryConfiguration);
if (!valid) {
const errors = (validateBotConfig.errors as DefinedError[]).filter(
(error) => !(error.keyword === "required" && error.params.missingProperty === "evmPrivateEncrypted")
);
const err = generateValidationError(errors as DefinedError[]);
const err = generateValidationError(validateBotConfig.errors as DefinedError[]);
if (err instanceof Error) throw err;
if (payload.issue?.number)
await context.octokit.issues.createComment({
Expand All @@ -72,7 +62,7 @@ export async function generateConfiguration(context: Context): Promise<BotConfig
repoConfig = repositoryConfiguration as BotConfig;
}

const merged = merge({}, orgConfig, repoConfig);
let merged = merge({}, orgConfig, repoConfig);
const valid = validateBotConfig(merged);
if (!valid) {
const err = generateValidationError(validateBotConfig.errors as DefinedError[]);
Expand All @@ -85,6 +75,16 @@ export async function generateConfiguration(context: Context): Promise<BotConfig
body: err,
});
}

// this will run transform functions
try {
merged = Value.Decode(BotConfigSchema, merged);
} catch (err) {
console.error(JSON.stringify(err, null, 2));
throw err;
}

console.dir(merged, { depth: null, colors: true });
return merged as BotConfig;
}

Expand All @@ -108,33 +108,12 @@ ${
return isValid ? message : new Error(message);
}

// async function fetchConfigurations(context: Context, type: "org" | "repo") {
// const payload = context.payload as Payload;
// let repo: string;
// let owner: string;
// if (type === "org") {
// repo = CONFIG_REPO;
// owner = payload.organization?.login || payload.repository.owner.login;
// } else {
// repo = payload.repository.name;
// owner = payload.repository.owner.login;
// }
// if (!repo || !owner) return null;
// const { data } = await context.octokit.rest.repos.getContent({
// owner,
// repo,
// path: CONFIG_PATH,
// mediaType: { format: "raw" },
// });
// return data as unknown as string; // not sure why the types are wrong but this is definitely returning a string
// }

async function download({
context,
repository,
owner,
}: {
context: Context;
context: ProbotContext;
repository: string;
owner: string;
}): Promise<string | null> {
Expand Down

0 comments on commit 4926daf

Please sign in to comment.