forked from ubiquity/ubiquibot
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathgenerate-configuration.ts
More file actions
151 lines (137 loc) · 4.87 KB
/
Copy pathgenerate-configuration.ts
File metadata and controls
151 lines (137 loc) · 4.87 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
import { Value } from "@sinclair/typebox/value";
import { DefinedError } from "ajv";
import mergeWith from "lodash/merge";
import { Context as ProbotContext } from "probot";
import YAML from "yaml";
import Runtime from "../bindings/bot-runtime";
import { BotConfig, stringDuration, validateBotConfig } from "../types/configuration-types";
import { GitHubPayload } from "../types/payload";
const UBIQUIBOT_CONFIG_REPOSITORY = "ubiquibot-config";
const UBIQUIBOT_CONFIG_FULL_PATH = ".github/ubiquibot-config.yml";
export async function generateConfiguration(context: ProbotContext): Promise<BotConfig> {
const payload = context.payload as GitHubPayload;
const orgConfig = parseYaml(
await download({
context,
repository: UBIQUIBOT_CONFIG_REPOSITORY,
owner: payload.organization?.login || payload.repository.owner.login,
})
);
const repoConfig = parseYaml(
await download({
context,
repository: payload.repository.name,
owner: payload.repository.owner.login,
})
);
const merged = mergeWith({}, orgConfig, repoConfig, (objValue: unknown, srcValue: unknown) => {
if (Array.isArray(objValue) && Array.isArray(srcValue)) {
// if it's string array, concat and remove duplicates
if (objValue.every((value) => typeof value === "string")) {
return [...new Set(objValue.concat(srcValue))];
}
// otherwise just concat
return objValue.concat(srcValue);
}
});
const logger = Runtime.getState().logger;
const isValid = validateBotConfig(merged);
if (!isValid) {
const errorMessage = getErrorMsg(validateBotConfig.errors as DefinedError[]);
if (errorMessage) {
throw logger.fatal("Invalid merged configuration", { errorMessage }, true);
}
}
// this will run transform functions
try {
transformConfig(merged);
} catch (err) {
if (err instanceof Error && payload.issue?.number) {
throw logger.fatal("Configuration error", { err }, true);
}
}
// console.dir(merged, { depth: null, colors: true });
return merged as BotConfig;
}
// Transforming the config only works with Typebox and not Ajv
// When you use Decode() it not only transforms the values but also validates the whole config and Typebox doesn't return all errors so we can filter for correct ones
// That's why we have transform every field manually and catch errors
export function transformConfig(config: BotConfig) {
let errorMsg = "";
try {
config.timers.reviewDelayTolerance = Value.Decode(stringDuration(), config.timers.reviewDelayTolerance);
} catch (err: unknown) {
const decodeError = err as DecodeError;
if (decodeError.value) {
errorMsg += `Invalid reviewDelayTolerance value: ${decodeError.value}\n`;
}
}
try {
config.timers.taskStaleTimeoutDuration = Value.Decode(stringDuration(), config.timers.taskStaleTimeoutDuration);
} catch (err: unknown) {
const decodeError = err as DecodeError;
if (decodeError.value) {
errorMsg += `Invalid taskStaleTimeoutDuration value: ${decodeError.value}\n`;
}
}
try {
config.timers.taskFollowUpDuration = Value.Decode(stringDuration(), config.timers.taskFollowUpDuration);
} catch (err: unknown) {
const decodeError = err as DecodeError;
if (decodeError.value) {
errorMsg += `Invalid taskFollowUpDuration value: ${decodeError.value}\n`;
}
}
try {
config.timers.taskDisqualifyDuration = Value.Decode(stringDuration(), config.timers.taskDisqualifyDuration);
} catch (err: unknown) {
const decodeError = err as DecodeError;
if (decodeError.value) {
errorMsg += `Invalid taskDisqualifyDuration value: ${decodeError.value}\n`;
}
}
if (errorMsg) throw new Error(errorMsg);
}
function getErrorMsg(errors: DefinedError[]) {
const errorsWithoutStrict = errors.filter((error) => error.keyword !== "additionalProperties");
return errorsWithoutStrict.length === 0
? null
: errorsWithoutStrict.map((error) => error.instancePath.replaceAll("/", ".") + " " + error.message).join("\n");
}
async function download({
context,
repository,
owner,
}: {
context: ProbotContext;
repository: string;
owner: string;
}): Promise<string | null> {
if (!repository || !owner) throw new Error("Repo or owner is not defined");
try {
const { data } = await context.octokit.rest.repos.getContent({
owner,
repo: repository,
path: UBIQUIBOT_CONFIG_FULL_PATH,
mediaType: { format: "raw" },
});
return data as unknown as string; // this will be a string if media format is raw
} catch (err) {
return null;
}
}
export function parseYaml(data: null | string) {
try {
if (data) {
const parsedData = YAML.parse(data);
return parsedData ?? null;
}
} catch (error) {
const logger = Runtime.getState().logger;
logger.fatal("Failed to parse YAML", { error });
}
return null;
}
interface DecodeError extends Error {
value?: string;
}