1 | import { __awaiter } from 'tslib';
|
2 | import Dataloader from 'dataloader';
|
3 | import yaml from 'js-yaml';
|
4 | import axios from 'axios';
|
5 | import { getIntrospectionQuery, printSchema, buildClientSchema, Source, buildSchema, parse, Kind, getLocation } from 'graphql';
|
6 | import { CriticalityLevel, diff as diff$1 } from '@graphql-inspector/core';
|
7 |
|
8 | function bolderize(msg) {
|
9 | return quotesTransformer(msg, '**');
|
10 | }
|
11 | function quotesTransformer(msg, symbols = '**') {
|
12 | const findSingleQuotes = /\'([^']+)\'/gim;
|
13 | const findDoubleQuotes = /\"([^"]+)\"/gim;
|
14 | function transformm(_, value) {
|
15 | return `${symbols}${value}${symbols}`;
|
16 | }
|
17 | return msg
|
18 | .replace(findSingleQuotes, transformm)
|
19 | .replace(findDoubleQuotes, transformm);
|
20 | }
|
21 | function slackCoderize(msg) {
|
22 | return quotesTransformer(msg, '`');
|
23 | }
|
24 | function discordCoderize(msg) {
|
25 | return quotesTransformer(msg, '`');
|
26 | }
|
27 | function filterChangesByLevel(level) {
|
28 | return (change) => change.criticality.level === level;
|
29 | }
|
30 | function createSummary(changes) {
|
31 | const breakingChanges = changes.filter(filterChangesByLevel(CriticalityLevel.Breaking));
|
32 | const dangerousChanges = changes.filter(filterChangesByLevel(CriticalityLevel.Dangerous));
|
33 | const safeChanges = changes.filter(filterChangesByLevel(CriticalityLevel.NonBreaking));
|
34 | const summary = [
|
35 | `# Found ${changes.length} change${changes.length > 1 ? 's' : ''}`,
|
36 | '',
|
37 | `Breaking: ${breakingChanges.length}`,
|
38 | `Dangerous: ${dangerousChanges.length}`,
|
39 | `Safe: ${safeChanges.length}`,
|
40 | ];
|
41 | function addChangesToSummary(type, changes) {
|
42 | summary.push(...['', `## ${type} changes`].concat(changes.map((change) => ` - ${bolderize(change.message)}`)));
|
43 | }
|
44 | if (breakingChanges.length) {
|
45 | addChangesToSummary('Breaking', breakingChanges);
|
46 | }
|
47 | if (dangerousChanges.length) {
|
48 | addChangesToSummary('Dangerous', dangerousChanges);
|
49 | }
|
50 | if (safeChanges.length) {
|
51 | addChangesToSummary('Safe', safeChanges);
|
52 | }
|
53 | summary.push([
|
54 | '',
|
55 | '___',
|
56 | `Thank you for using [GraphQL Inspector](https://graphql-inspector.com/)`,
|
57 | `If you like it, [consider supporting the project](https://github.com/sponsors/kamilkisiela).`,
|
58 | ].join('\n'));
|
59 | return summary.join('\n');
|
60 | }
|
61 | function isNil(val) {
|
62 | return !val && typeof val !== 'boolean';
|
63 | }
|
64 | function parseEndpoint(endpoint) {
|
65 | if (typeof endpoint === 'string') {
|
66 | return {
|
67 | url: endpoint,
|
68 | method: 'POST',
|
69 | };
|
70 | }
|
71 | return {
|
72 | url: endpoint.url,
|
73 | method: endpoint.method || 'POST',
|
74 | headers: endpoint.headers,
|
75 | };
|
76 | }
|
77 | function batch(items, limit) {
|
78 | const batches = [];
|
79 | const batchesNum = Math.ceil(items.length / limit);
|
80 |
|
81 | if (batchesNum === 0) {
|
82 | return [[]];
|
83 | }
|
84 | for (let i = 0; i < batchesNum; i++) {
|
85 | const start = i * limit;
|
86 | const end = start + limit;
|
87 | batches.push(items.slice(start, end));
|
88 | }
|
89 | return batches;
|
90 | }
|
91 | function objectFromEntries(iterable) {
|
92 | return [...iterable].reduce((obj, [key, val]) => {
|
93 | obj[key] = val;
|
94 | return obj;
|
95 | }, {});
|
96 | }
|
97 |
|
98 | function createGetFilesQuery(variableMap) {
|
99 | const variables = Object.keys(variableMap)
|
100 | .map((name) => `$${name}: String!`)
|
101 | .join(', ');
|
102 | const files = Object.keys(variableMap)
|
103 | .map((name) => {
|
104 | return `
|
105 | ${name}: object(expression: $${name}) {
|
106 | ... on Blob {
|
107 | text
|
108 | }
|
109 | }
|
110 | `;
|
111 | })
|
112 | .join('\n');
|
113 | return `
|
114 | query GetFile($repo: String!, $owner: String!, ${variables}) {
|
115 | repository(name: $repo, owner: $owner) {
|
116 | ${files}
|
117 | }
|
118 | }
|
119 | `.replace(/\s+/g, ' ');
|
120 | }
|
121 | function createFileLoader(config) {
|
122 | const loader = new Dataloader((inputs) => __awaiter(this, void 0, void 0, function* () {
|
123 | const variablesMap = objectFromEntries(inputs.map((input) => [input.alias, `${input.ref}:${input.path}`]));
|
124 | const { context, repo, owner } = config;
|
125 | const result = yield context.github.graphql(createGetFilesQuery(variablesMap), Object.assign({ repo,
|
126 | owner }, variablesMap));
|
127 | return Promise.all(inputs.map((input) => __awaiter(this, void 0, void 0, function* () {
|
128 | const alias = input.alias;
|
129 | try {
|
130 | if (!result) {
|
131 | throw new Error(`No result :(`);
|
132 | }
|
133 | if (result.data) {
|
134 | return result.data.repository[alias].text;
|
135 | }
|
136 | return result.repository[alias].text;
|
137 | }
|
138 | catch (error) {
|
139 | const failure = new Error(`Failed to load '${input.path}' (ref: ${input.ref})`);
|
140 | if (input.throwNotFound === false) {
|
141 | if (input.onError) {
|
142 | input.onError(failure);
|
143 | }
|
144 | else {
|
145 | console.error(failure);
|
146 | }
|
147 | return null;
|
148 | }
|
149 | throw failure;
|
150 | }
|
151 | })));
|
152 | }), {
|
153 | batch: true,
|
154 | maxBatchSize: 5,
|
155 | cacheKeyFn(obj) {
|
156 | return `${obj.ref} - ${obj.path}`;
|
157 | },
|
158 | });
|
159 | return (input) => loader.load(input);
|
160 | }
|
161 | function createConfigLoader(config, loadFile) {
|
162 | const loader = new Dataloader((ids) => {
|
163 | const errors = [];
|
164 | const onError = (error) => {
|
165 | errors.push(error);
|
166 | };
|
167 | return Promise.all(ids.map((id) => __awaiter(this, void 0, void 0, function* () {
|
168 | const [yamlConfig, ymlConfig, pkgFile] = yield Promise.all([
|
169 | loadFile(Object.assign(Object.assign({}, config), { alias: 'yaml', path: `.github/${id}.yaml`, throwNotFound: false, onError })),
|
170 | loadFile(Object.assign(Object.assign({}, config), { alias: 'yml', path: `.github/${id}.yml`, throwNotFound: false, onError })),
|
171 | loadFile(Object.assign(Object.assign({}, config), { alias: 'pkg', path: 'package.json', throwNotFound: false, onError })),
|
172 | ]);
|
173 | if (yamlConfig || ymlConfig) {
|
174 | return yaml.safeLoad((yamlConfig || ymlConfig));
|
175 | }
|
176 | if (pkgFile) {
|
177 | try {
|
178 | const pkg = JSON.parse(pkgFile);
|
179 | if (pkg[id]) {
|
180 | return pkg[id];
|
181 | }
|
182 | }
|
183 | catch (error) {
|
184 | errors.push(error);
|
185 | }
|
186 | }
|
187 | console.error([`Failed to load config:`, ...errors].join('\n'));
|
188 | return null;
|
189 | })));
|
190 | }, {
|
191 | batch: false,
|
192 | });
|
193 | return () => loader.load('graphql-inspector');
|
194 | }
|
195 | function printSchemaFromEndpoint(endpoint) {
|
196 | return __awaiter(this, void 0, void 0, function* () {
|
197 | const config = parseEndpoint(endpoint);
|
198 | const { data: response } = yield axios.request({
|
199 | method: config.method,
|
200 | url: config.url,
|
201 | headers: config.headers,
|
202 | data: {
|
203 | query: getIntrospectionQuery().replace(/\s+/g, ' ').trim(),
|
204 | },
|
205 | });
|
206 | const introspection = response.data;
|
207 | return printSchema(buildClientSchema(introspection, {
|
208 | assumeValid: true,
|
209 | }));
|
210 | });
|
211 | }
|
212 | function loadSources({ config, oldPointer, newPointer, loadFile, }) {
|
213 | return __awaiter(this, void 0, void 0, function* () {
|
214 |
|
215 |
|
216 | const useEndpoint = !isNil(config.endpoint);
|
217 | const [oldFile, newFile] = yield Promise.all([
|
218 | useEndpoint
|
219 | ? printSchemaFromEndpoint(config.endpoint)
|
220 | : loadFile(Object.assign(Object.assign({}, oldPointer), { alias: 'oldSource' })),
|
221 | loadFile(Object.assign(Object.assign({}, newPointer), { alias: 'newSource' })),
|
222 | ]);
|
223 | return {
|
224 | old: new Source(oldFile),
|
225 | new: new Source(newFile),
|
226 | };
|
227 | });
|
228 | }
|
229 |
|
230 | const defaultConfigName = '__default';
|
231 | const defaultFallbackBranch = '*';
|
232 | const diffDefault = {
|
233 | annotations: true,
|
234 | failOnBreaking: true,
|
235 | };
|
236 | const notificationsDefault = false;
|
237 | function normalizeConfig(config) {
|
238 | if (isLegacyConfig(config)) {
|
239 | console.log('config type - "legacy"');
|
240 | return {
|
241 | [defaultConfigName]: {
|
242 | name: defaultConfigName,
|
243 | schema: config.schema.path,
|
244 | branch: config.schema.ref,
|
245 | endpoint: config.endpoint,
|
246 | notifications: prioritize(config.notifications, notificationsDefault),
|
247 | diff: prioritize(config.diff, diffDefault),
|
248 | },
|
249 | };
|
250 | }
|
251 | if (isSingleEnvironmentConfig(config)) {
|
252 | console.log('config type - "single"');
|
253 | return {
|
254 | [config.branch]: {
|
255 | name: config.branch,
|
256 | schema: config.schema,
|
257 | branch: config.branch,
|
258 | endpoint: config.endpoint,
|
259 | notifications: prioritize(config.notifications, notificationsDefault),
|
260 | diff: prioritize(config.diff, diffDefault),
|
261 | },
|
262 | };
|
263 | }
|
264 | if (isMultipleEnvironmentConfig(config)) {
|
265 | console.log('config type - "multiple"');
|
266 | const normalized = {};
|
267 | for (const envName in config.env) {
|
268 | if (config.env.hasOwnProperty(envName)) {
|
269 | const env = config.env[envName];
|
270 | normalized[envName] = {
|
271 | name: envName,
|
272 | schema: config.schema,
|
273 | branch: env.branch,
|
274 | endpoint: env.endpoint,
|
275 | diff: prioritize(env.diff, config.diff, diffDefault),
|
276 | notifications: prioritize(env.notifications, config.notifications, notificationsDefault),
|
277 | };
|
278 | }
|
279 | }
|
280 | return normalized;
|
281 | }
|
282 | throw new Error('Invalid configuration');
|
283 | }
|
284 | function getGlobalConfig(config, fallbackBranch) {
|
285 | var _a;
|
286 | return {
|
287 | name: 'global',
|
288 | schema: config.schema,
|
289 | branch: fallbackBranch,
|
290 | notifications: false,
|
291 | diff: prioritize((_a = config.others) === null || _a === void 0 ? void 0 : _a.diff, config.diff, diffDefault),
|
292 | };
|
293 | }
|
294 | function createConfig(rawConfig, branches = [], fallbackBranch = defaultFallbackBranch) {
|
295 | const normalizedConfig = normalizeConfig(rawConfig);
|
296 | let config = null;
|
297 | if (isNormalizedLegacyConfig(normalizedConfig)) {
|
298 | config = normalizedConfig[defaultConfigName];
|
299 | if (branches.includes(config.branch) === false) {
|
300 | config.endpoint = undefined;
|
301 | }
|
302 | return config;
|
303 | }
|
304 | for (const branch of branches) {
|
305 | if (!config) {
|
306 | config = findConfigByBranch(branch, normalizedConfig, false);
|
307 | }
|
308 | if (config) {
|
309 | break;
|
310 | }
|
311 | }
|
312 | if (!config) {
|
313 | config = getGlobalConfig(rawConfig, fallbackBranch);
|
314 | }
|
315 | return config;
|
316 | }
|
317 | function isNormalizedLegacyConfig(config) {
|
318 | return typeof config[defaultConfigName] === 'object';
|
319 | }
|
320 | function isLegacyConfig(config) {
|
321 | return config.schema && typeof config.schema === 'object';
|
322 | }
|
323 | function isSingleEnvironmentConfig(config) {
|
324 | return !config.env;
|
325 | }
|
326 | function isMultipleEnvironmentConfig(config) {
|
327 | return !isLegacyConfig(config) && !isSingleEnvironmentConfig(config);
|
328 | }
|
329 | function findConfigByBranch(branch, config, throwOnMissing = true) {
|
330 | const branches = [];
|
331 | for (const name in config) {
|
332 | if (config.hasOwnProperty(name)) {
|
333 | const env = config[name];
|
334 | if (env.branch === branch) {
|
335 | return env;
|
336 | }
|
337 | branches.push(env.branch);
|
338 | }
|
339 | }
|
340 | if (throwOnMissing) {
|
341 | throw new Error(`Couldn't match branch "${branch}" with branches in config. Available branches: ${branches.join(', ')}`);
|
342 | }
|
343 | return null;
|
344 | }
|
345 |
|
346 | function prioritize(child, parent, defaults) {
|
347 | if (child === false) {
|
348 | return false;
|
349 | }
|
350 | if (child === true || isNil(child)) {
|
351 | if (parent === true || isNil(parent)) {
|
352 | return defaults || false;
|
353 | }
|
354 | return typeof parent === 'object' && typeof defaults === 'object'
|
355 | ? Object.assign(Object.assign({}, defaults), parent) : parent;
|
356 | }
|
357 | if (parent && typeof parent === 'object') {
|
358 | return Object.assign(Object.assign(Object.assign({}, defaults), parent), child);
|
359 | }
|
360 | return typeof child === 'object' && typeof defaults === 'object'
|
361 | ? Object.assign(Object.assign({}, defaults), child) : child;
|
362 | }
|
363 |
|
364 | function notifyWithWebhook({ url, changes, environment, }) {
|
365 | return __awaiter(this, void 0, void 0, function* () {
|
366 | const event = {
|
367 | environment: environment && environment !== defaultConfigName
|
368 | ? environment
|
369 | : 'default',
|
370 | changes: changes.map((change) => ({
|
371 | message: change.message,
|
372 | level: change.criticality.level,
|
373 | })),
|
374 | };
|
375 | yield axios.post(url, event, {
|
376 | headers: {
|
377 | 'content-type': 'application/json',
|
378 | },
|
379 | });
|
380 | });
|
381 | }
|
382 | function notifyWithSlack({ url, changes, environment, }) {
|
383 | return __awaiter(this, void 0, void 0, function* () {
|
384 | const totalChanges = changes.length;
|
385 | const schemaName = environment ? `${environment} schema` : `schema`;
|
386 | const event = {
|
387 | username: 'GraphQL Inspector',
|
388 | icon_url: 'https://graphql-inspector/img/logo-slack.png',
|
389 | text: `:male-detective: Hi, I found *${totalChanges} ${pluralize('change', totalChanges)}* ${schemaName}:`,
|
390 | attachments: createAttachments(changes),
|
391 | };
|
392 | yield axios.post(url, event, {
|
393 | headers: {
|
394 | 'content-type': 'application/json',
|
395 | },
|
396 | });
|
397 | });
|
398 | }
|
399 | function notifyWithDiscord({ url, changes, environment, }) {
|
400 | return __awaiter(this, void 0, void 0, function* () {
|
401 | const totalChanges = changes.length;
|
402 | const schemaName = environment ? `${environment} schema` : `schema`;
|
403 | const event = {
|
404 | username: 'GraphQL Inspector',
|
405 | avatar_url: 'https://graphql-inspector/img/logo-slack.png',
|
406 | content: `:detective: Hi, I found **${totalChanges} ${pluralize('change', totalChanges)}** in ${schemaName}:`,
|
407 | embeds: createDiscordEmbeds(changes),
|
408 | };
|
409 | yield axios.post(url, event, {
|
410 | headers: {
|
411 | 'content-type': 'application/json',
|
412 | },
|
413 | });
|
414 | });
|
415 | }
|
416 | function createAttachments(changes) {
|
417 | const breakingChanges = changes.filter(filterChangesByLevel(CriticalityLevel.Breaking));
|
418 | const dangerousChanges = changes.filter(filterChangesByLevel(CriticalityLevel.Dangerous));
|
419 | const safeChanges = changes.filter(filterChangesByLevel(CriticalityLevel.NonBreaking));
|
420 | const attachments = [];
|
421 | if (breakingChanges.length) {
|
422 | attachments.push(renderAttachments({
|
423 | color: '#E74C3B',
|
424 | title: 'Breaking changes',
|
425 | changes: breakingChanges,
|
426 | }));
|
427 | }
|
428 | if (dangerousChanges.length) {
|
429 | attachments.push(renderAttachments({
|
430 | color: '#F0C418',
|
431 | title: 'Dangerous changes',
|
432 | changes: dangerousChanges,
|
433 | }));
|
434 | }
|
435 | if (safeChanges.length) {
|
436 | attachments.push(renderAttachments({
|
437 | color: '#23B99A',
|
438 | title: 'Safe changes',
|
439 | changes: safeChanges,
|
440 | }));
|
441 | }
|
442 | return attachments;
|
443 | }
|
444 | function renderAttachments({ changes, title, color, }) {
|
445 | const text = changes
|
446 | .map((change) => slackCoderize(change.message))
|
447 | .join('\n');
|
448 | return {
|
449 | mrkdwn_in: ['text', 'fallback'],
|
450 | color,
|
451 | author_name: title,
|
452 | text,
|
453 | fallback: text,
|
454 | };
|
455 | }
|
456 | function createDiscordEmbeds(changes) {
|
457 | const breakingChanges = changes.filter(filterChangesByLevel(CriticalityLevel.Breaking));
|
458 | const dangerousChanges = changes.filter(filterChangesByLevel(CriticalityLevel.Dangerous));
|
459 | const safeChanges = changes.filter(filterChangesByLevel(CriticalityLevel.NonBreaking));
|
460 | const embeds = [];
|
461 | if (breakingChanges.length) {
|
462 | embeds.push(renderDiscordEmbed({
|
463 | color: 15158331,
|
464 | title: 'Breaking changes',
|
465 | changes: breakingChanges,
|
466 | }));
|
467 | }
|
468 | if (dangerousChanges.length) {
|
469 | embeds.push(renderDiscordEmbed({
|
470 | color: 15778840,
|
471 | title: 'Dangerous changes',
|
472 | changes: dangerousChanges,
|
473 | }));
|
474 | }
|
475 | if (safeChanges.length) {
|
476 | embeds.push(renderDiscordEmbed({
|
477 | color: 2341274,
|
478 | title: 'Safe changes',
|
479 | changes: safeChanges,
|
480 | }));
|
481 | }
|
482 | return embeds;
|
483 | }
|
484 | function renderDiscordEmbed({ changes, title, color, }) {
|
485 | const description = changes
|
486 | .map((change) => discordCoderize(change.message))
|
487 | .join('\n');
|
488 | return {
|
489 | color,
|
490 | title,
|
491 | description,
|
492 | };
|
493 | }
|
494 | function pluralize(word, num) {
|
495 | return word + (num > 1 ? 's' : '');
|
496 | }
|
497 |
|
498 | function createLogger(label, context) {
|
499 | const id = Math.random().toString(16).substr(2, 5);
|
500 | const prefix = (msg) => `${label} ${id} : ${msg}`;
|
501 | return {
|
502 | log(msg) {
|
503 | context.log(prefix(msg));
|
504 | },
|
505 | info(msg) {
|
506 | context.log.info(prefix(msg));
|
507 | },
|
508 | warn(msg) {
|
509 | context.log.warn(prefix(msg));
|
510 | },
|
511 | error(msg, error) {
|
512 | if (error) {
|
513 | console.error(error);
|
514 | }
|
515 | context.log.error(prefix(msg instanceof Error ? msg.message : msg));
|
516 | },
|
517 | };
|
518 | }
|
519 |
|
520 | function handleSchemaChangeNotifications({ context, ref, repo, owner, before, loadFile, loadConfig, }) {
|
521 | return __awaiter(this, void 0, void 0, function* () {
|
522 | const id = `${owner}/${repo}#${ref}`;
|
523 | const logger = createLogger('NOTIFICATIONS', context);
|
524 | logger.info(`started - ${id}`);
|
525 | const isBranchPush = ref.startsWith('refs/heads/');
|
526 | if (!isBranchPush) {
|
527 | logger.warn(`Received Push event is not a branch push event (ref "${ref}")`);
|
528 | return;
|
529 | }
|
530 | const rawConfig = yield loadConfig();
|
531 | if (!rawConfig) {
|
532 | logger.error(`Missing config file`);
|
533 | return;
|
534 | }
|
535 | const branch = ref.replace('refs/heads/', '');
|
536 | const config = createConfig(rawConfig, [branch]);
|
537 | if (!config.notifications) {
|
538 | logger.info(`disabled. Skipping...`);
|
539 | return;
|
540 | }
|
541 | else {
|
542 | logger.info(`enabled`);
|
543 | }
|
544 | if (config.branch !== branch) {
|
545 | logger.info(`Received branch "${branch}" doesn't match expected branch "${config.branch}". Skipping...`);
|
546 | return;
|
547 | }
|
548 | const oldPointer = {
|
549 | path: config.schema,
|
550 | ref: before,
|
551 | };
|
552 | const newPointer = {
|
553 | path: config.schema,
|
554 | ref,
|
555 | };
|
556 | const sources = yield loadSources({ config, oldPointer, newPointer, loadFile });
|
557 | const schemas = {
|
558 | old: buildSchema(sources.old, {
|
559 | assumeValid: true,
|
560 | assumeValidSDL: true,
|
561 | }),
|
562 | new: buildSchema(sources.new, {
|
563 | assumeValid: true,
|
564 | assumeValidSDL: true,
|
565 | }),
|
566 | };
|
567 | logger.info(`built schemas`);
|
568 | const changes = diff$1(schemas.old, schemas.new);
|
569 | if (!changes.length) {
|
570 | logger.info(`schemas are equal. Skipping...`);
|
571 | return;
|
572 | }
|
573 | const notifications = config.notifications;
|
574 | if (hasNotificationsEnabled(notifications)) {
|
575 | function actionRunner(target, fn) {
|
576 | return __awaiter(this, void 0, void 0, function* () {
|
577 | try {
|
578 | yield fn();
|
579 | }
|
580 | catch (error) {
|
581 | logger.error(`Failed to send a notification via ${target}`, error);
|
582 | }
|
583 | });
|
584 | }
|
585 | const actions = [];
|
586 | if (notifications.slack) {
|
587 | actions.push(actionRunner('slack', () => notifyWithSlack({
|
588 | url: notifications.slack,
|
589 | changes,
|
590 | environment: config.name,
|
591 | })));
|
592 | }
|
593 | if (notifications.discord) {
|
594 | actions.push(actionRunner('slack', () => notifyWithDiscord({
|
595 | url: notifications.slack,
|
596 | changes,
|
597 | environment: config.name,
|
598 | })));
|
599 | }
|
600 | if (notifications.webhook) {
|
601 | actions.push(actionRunner('webhook', () => notifyWithWebhook({
|
602 | url: notifications.webhook,
|
603 | changes,
|
604 | environment: config.name,
|
605 | })));
|
606 | }
|
607 | if (actions.length) {
|
608 | yield Promise.all(actions);
|
609 | }
|
610 | }
|
611 | });
|
612 | }
|
613 | function hasNotificationsEnabled(notifications) {
|
614 | return notifications && typeof notifications === 'object';
|
615 | }
|
616 |
|
617 | var AnnotationLevel;
|
618 | (function (AnnotationLevel) {
|
619 | AnnotationLevel["Failure"] = "failure";
|
620 | AnnotationLevel["Warning"] = "warning";
|
621 | AnnotationLevel["Notice"] = "notice";
|
622 | })(AnnotationLevel || (AnnotationLevel = {}));
|
623 | var CheckStatus;
|
624 | (function (CheckStatus) {
|
625 | CheckStatus["InProgress"] = "in_progress";
|
626 | CheckStatus["Completed"] = "completed";
|
627 | })(CheckStatus || (CheckStatus = {}));
|
628 | var CheckConclusion;
|
629 | (function (CheckConclusion) {
|
630 | CheckConclusion["Success"] = "success";
|
631 | CheckConclusion["Neutral"] = "neutral";
|
632 | CheckConclusion["Failure"] = "failure";
|
633 | })(CheckConclusion || (CheckConclusion = {}));
|
634 |
|
635 | const headers = { accept: 'application/vnd.github.antiope-preview+json' };
|
636 | function start({ context, owner, repo, sha, logger, }) {
|
637 | return __awaiter(this, void 0, void 0, function* () {
|
638 | try {
|
639 | const result = yield context.github.request({
|
640 | headers,
|
641 | method: 'POST',
|
642 | url: `https://api.github.com/repos/${owner}/${repo}/check-runs`,
|
643 | name: 'graphql-inspector',
|
644 | started_at: new Date().toISOString(),
|
645 | head_sha: sha,
|
646 | status: CheckStatus.InProgress,
|
647 | });
|
648 | logger.info(`check started`);
|
649 | return result.data.url;
|
650 | }
|
651 | catch (error) {
|
652 | logger.error(`failed to start a check`, error);
|
653 | throw error;
|
654 | }
|
655 | });
|
656 | }
|
657 | function annotate({ context, url, annotations, title, summary, logger, }) {
|
658 | return __awaiter(this, void 0, void 0, function* () {
|
659 | const batches = batch(annotations, 50);
|
660 | context.log.info(`annotations to be sent: ${annotations.length}`);
|
661 | context.log.info(`title: ${title}`);
|
662 | try {
|
663 | yield Promise.all(batches.map((chunk) => __awaiter(this, void 0, void 0, function* () {
|
664 | yield context.github.request({
|
665 | headers,
|
666 | url,
|
667 | method: 'PATCH',
|
668 | output: {
|
669 | annotations: chunk,
|
670 | title,
|
671 | summary,
|
672 | },
|
673 | });
|
674 | logger.info(`annotations sent (${chunk.length})`);
|
675 | })));
|
676 | }
|
677 | catch (error) {
|
678 | logger.error(`failed to send annotations`, error);
|
679 | throw error;
|
680 | }
|
681 | });
|
682 | }
|
683 | function complete({ context, url, conclusion, logger, }) {
|
684 | return __awaiter(this, void 0, void 0, function* () {
|
685 | try {
|
686 | yield context.github.request({
|
687 | headers,
|
688 | url,
|
689 | conclusion,
|
690 | method: 'PATCH',
|
691 | completed_at: new Date().toISOString(),
|
692 | status: CheckStatus.Completed,
|
693 | });
|
694 | logger.info(`check completed`);
|
695 | }
|
696 | catch (error) {
|
697 | logger.error(`failed to complete a check`, error);
|
698 | throw error;
|
699 | }
|
700 | });
|
701 | }
|
702 |
|
703 | function getLocationByPath({ path, source, }) {
|
704 | const [typeName, ...rest] = path.split('.');
|
705 | const isDirective = typeName.startsWith('@');
|
706 | const doc = parse(source);
|
707 | let resolvedNode = undefined;
|
708 | for (const definition of doc.definitions) {
|
709 | if (definition.kind === Kind.OBJECT_TYPE_DEFINITION &&
|
710 | definition.name.value === typeName) {
|
711 | resolvedNode = resolveObjectTypeDefinition(rest, definition);
|
712 | break;
|
713 | }
|
714 | if (isDirective &&
|
715 | definition.kind === Kind.DIRECTIVE_DEFINITION &&
|
716 | definition.name.value === typeName.substring(1)) {
|
717 | resolvedNode = resolveDirectiveDefinition(rest, definition);
|
718 | break;
|
719 | }
|
720 | if (definition.kind === Kind.ENUM_TYPE_DEFINITION &&
|
721 | definition.name.value === typeName) {
|
722 | resolvedNode = resolveEnumTypeDefinition(rest, definition);
|
723 | break;
|
724 | }
|
725 | if (definition.kind === Kind.INPUT_OBJECT_TYPE_DEFINITION &&
|
726 | definition.name.value === typeName) {
|
727 | resolvedNode = resolveInputObjectTypeDefinition(rest, definition);
|
728 | break;
|
729 | }
|
730 | if (definition.kind === Kind.INTERFACE_TYPE_DEFINITION &&
|
731 | definition.name.value === typeName) {
|
732 | resolvedNode = resolveInterfaceTypeDefinition(rest, definition);
|
733 | break;
|
734 | }
|
735 | if (definition.kind === Kind.UNION_TYPE_DEFINITION &&
|
736 | definition.name.value === typeName) {
|
737 | resolvedNode = resolveUnionTypeDefinitionNode(rest, definition);
|
738 | break;
|
739 | }
|
740 | if (definition.kind === Kind.SCALAR_TYPE_DEFINITION &&
|
741 | definition.name.value === typeName) {
|
742 | resolvedNode = resolveScalarTypeDefinitionNode(rest, definition);
|
743 | break;
|
744 | }
|
745 | }
|
746 | return resolveNodeSourceLocation(source, resolvedNode);
|
747 | }
|
748 | function resolveScalarTypeDefinitionNode(_path, definition) {
|
749 | return definition;
|
750 | }
|
751 | function resolveUnionTypeDefinitionNode(_path, definition) {
|
752 | return definition;
|
753 | }
|
754 | function resolveArgument(argName, field) {
|
755 | var _a;
|
756 | const arg = (_a = field.arguments) === null || _a === void 0 ? void 0 : _a.find((a) => a.name.value === argName);
|
757 | return arg || field;
|
758 | }
|
759 | function resolveFieldDefinition(path, definition) {
|
760 | var _a;
|
761 | const [fieldName, argName] = path;
|
762 | const fieldIndex = (_a = definition.fields) === null || _a === void 0 ? void 0 : _a.findIndex((f) => f.name.value === fieldName);
|
763 | if (typeof fieldIndex === 'number' && fieldIndex > -1) {
|
764 | const field = definition.fields[fieldIndex];
|
765 | if (field.kind !== Kind.INPUT_VALUE_DEFINITION && argName) {
|
766 | return resolveArgument(argName, field);
|
767 | }
|
768 | return field;
|
769 | }
|
770 | return definition;
|
771 | }
|
772 | function resolveInterfaceTypeDefinition(path, definition) {
|
773 | const [fieldName, argName] = path;
|
774 | if (fieldName) {
|
775 | return resolveFieldDefinition([fieldName, argName], definition);
|
776 | }
|
777 | return definition;
|
778 | }
|
779 | function resolveInputObjectTypeDefinition(path, definition) {
|
780 | const [fieldName] = path;
|
781 | if (fieldName) {
|
782 | return resolveFieldDefinition([fieldName], definition);
|
783 | }
|
784 | return definition;
|
785 | }
|
786 | function resolveEnumTypeDefinition(path, definition) {
|
787 | const [valueName] = path;
|
788 | if (definition.values && valueName) {
|
789 | const value = definition.values.find((val) => val.name.value === valueName);
|
790 | if (value) {
|
791 | return value;
|
792 | }
|
793 | }
|
794 | return definition;
|
795 | }
|
796 | function resolveObjectTypeDefinition(path, definition) {
|
797 | const [fieldName, argName] = path;
|
798 | if (fieldName) {
|
799 | return resolveFieldDefinition([fieldName, argName], definition);
|
800 | }
|
801 | return definition;
|
802 | }
|
803 | function resolveDirectiveDefinition(path, defininition) {
|
804 | const [argName] = path;
|
805 | if (defininition.arguments && argName) {
|
806 | const arg = defininition.arguments.find((arg) => arg.name.value === argName);
|
807 | if (arg) {
|
808 | return arg;
|
809 | }
|
810 | }
|
811 | return defininition;
|
812 | }
|
813 | function resolveNodeSourceLocation(source, node) {
|
814 | if (!node || !node.loc) {
|
815 | return {
|
816 | line: 1,
|
817 | column: 1,
|
818 | };
|
819 | }
|
820 | const nodeLocation = getLocation(source, node.loc.start);
|
821 | if (node.description && node.description.loc) {
|
822 | return {
|
823 | line: getLocation(source, node.description.loc.end).line + 1,
|
824 | column: nodeLocation.column,
|
825 | };
|
826 | }
|
827 | return nodeLocation;
|
828 | }
|
829 |
|
830 | function diff({ path, schemas, sources, interceptor, pullRequests, ref, }) {
|
831 | return __awaiter(this, void 0, void 0, function* () {
|
832 | let changes = diff$1(schemas.old, schemas.new);
|
833 | let forcedConclusion = null;
|
834 | if (!changes || !changes.length) {
|
835 | return {
|
836 | conclusion: CheckConclusion.Success,
|
837 | };
|
838 | }
|
839 | if (!isNil(interceptor)) {
|
840 | const interceptionResult = yield interceptChanges(interceptor, {
|
841 | pullRequests,
|
842 | ref,
|
843 | changes,
|
844 | });
|
845 | changes = interceptionResult.changes || [];
|
846 | forcedConclusion = interceptionResult.conclusion || null;
|
847 | }
|
848 | const annotations = yield Promise.all(changes.map((change) => annotate$1({ path, change, source: sources.new })));
|
849 | let conclusion = CheckConclusion.Success;
|
850 | if (changes.some((change) => change.criticality.level === CriticalityLevel.Breaking)) {
|
851 | conclusion = CheckConclusion.Failure;
|
852 | }
|
853 | if (forcedConclusion) {
|
854 | conclusion = forcedConclusion;
|
855 | }
|
856 | return {
|
857 | conclusion,
|
858 | annotations,
|
859 | changes,
|
860 | };
|
861 | });
|
862 | }
|
863 | const levelMap = {
|
864 | [CriticalityLevel.Breaking]: AnnotationLevel.Failure,
|
865 | [CriticalityLevel.Dangerous]: AnnotationLevel.Warning,
|
866 | [CriticalityLevel.NonBreaking]: AnnotationLevel.Notice,
|
867 | };
|
868 | function annotate$1({ path, change, source, }) {
|
869 | const level = change.criticality.level;
|
870 | const loc = change.path
|
871 | ? getLocationByPath({ path: change.path, source })
|
872 | : { line: 1, column: 1 };
|
873 | return {
|
874 | title: change.message,
|
875 | annotation_level: levelMap[level],
|
876 | path,
|
877 | message: change.criticality.reason || change.message,
|
878 | start_line: loc.line,
|
879 | end_line: loc.line,
|
880 | };
|
881 | }
|
882 | function interceptChanges(interceptor, payload) {
|
883 | return __awaiter(this, void 0, void 0, function* () {
|
884 | const endpoint = parseEndpoint(interceptor);
|
885 | const { data } = yield axios.request({
|
886 | url: endpoint.url,
|
887 | method: endpoint.method,
|
888 | data: payload,
|
889 | });
|
890 | return data;
|
891 | });
|
892 | }
|
893 |
|
894 | class MissingConfigError extends Error {
|
895 | constructor() {
|
896 | super([
|
897 | 'Failed to find a configuration',
|
898 | '',
|
899 | 'https://graphql-inspector.com/docs/products/github#usage',
|
900 | ].join('\n'));
|
901 | }
|
902 | }
|
903 |
|
904 | function handleSchemaDiff({ action, context, ref, pullRequestNumber, repo, owner, before, pullRequests = [], loadFile, loadConfig, }) {
|
905 | var _a;
|
906 | return __awaiter(this, void 0, void 0, function* () {
|
907 | const id = `${owner}/${repo}#${ref}`;
|
908 | const logger = createLogger('DIFF', context);
|
909 | logger.info(`Started - ${id}`);
|
910 | logger.info(`Action: "${action}"`);
|
911 | const checkUrl = yield start({
|
912 | context,
|
913 | owner,
|
914 | repo,
|
915 | sha: ref,
|
916 | logger,
|
917 | });
|
918 | try {
|
919 | logger.info(`Looking for config`);
|
920 | const rawConfig = yield loadConfig();
|
921 | if (!rawConfig) {
|
922 | logger.error(`Config file missing`);
|
923 | throw new MissingConfigError();
|
924 | }
|
925 | const branches = pullRequests.map((pr) => pr.base.ref);
|
926 | const firstBranch = branches[0];
|
927 | const fallbackBranch = firstBranch || before;
|
928 | logger.info(`fallback branch from Pull Requests: ${firstBranch}`);
|
929 | logger.info(`SHA before push: ${before}`);
|
930 |
|
931 | const config = createConfig(rawConfig, branches, fallbackBranch);
|
932 | if (!config.diff) {
|
933 | logger.info(`disabled. Skipping...`);
|
934 | yield complete({
|
935 | url: checkUrl,
|
936 | context,
|
937 | conclusion: CheckConclusion.Success,
|
938 | logger,
|
939 | });
|
940 | return;
|
941 | }
|
942 | else {
|
943 | logger.info(`enabled`);
|
944 | }
|
945 | if (!config.branch || /^[0]+$/.test(config.branch)) {
|
946 | logger.info(`Nothing to compare with. Skipping...`);
|
947 | yield complete({
|
948 | url: checkUrl,
|
949 | context,
|
950 | conclusion: CheckConclusion.Success,
|
951 | logger,
|
952 | });
|
953 | return;
|
954 | }
|
955 | if (config.diff.experimental_merge) {
|
956 | if (!pullRequestNumber && (pullRequests === null || pullRequests === void 0 ? void 0 : pullRequests.length)) {
|
957 | pullRequestNumber = pullRequests[0].number;
|
958 | }
|
959 | if (pullRequestNumber) {
|
960 | ref = `refs/pull/${pullRequestNumber}/merge`;
|
961 | logger.info(`[EXPERIMENTAL] Using Pull Request: ${ref}`);
|
962 | }
|
963 | }
|
964 | const oldPointer = {
|
965 | path: config.schema,
|
966 | ref: config.branch,
|
967 | };
|
968 | const newPointer = {
|
969 | path: oldPointer.path,
|
970 | ref,
|
971 | };
|
972 | if (oldPointer.ref === defaultFallbackBranch) {
|
973 | logger.error('used default ref to get old schema');
|
974 | }
|
975 | if (newPointer.ref === defaultFallbackBranch) {
|
976 | logger.error('used default ref to get new schema');
|
977 | }
|
978 | const sources = yield loadSources({
|
979 | config,
|
980 | oldPointer,
|
981 | newPointer,
|
982 | loadFile,
|
983 | });
|
984 | const schemas = {
|
985 | old: buildSchema(sources.old, {
|
986 | assumeValid: true,
|
987 | assumeValidSDL: true,
|
988 | }),
|
989 | new: buildSchema(sources.new, {
|
990 | assumeValid: true,
|
991 | assumeValidSDL: true,
|
992 | }),
|
993 | };
|
994 | logger.info(`built schemas`);
|
995 | const action = yield diff({
|
996 | path: config.schema,
|
997 | schemas,
|
998 | sources,
|
999 | });
|
1000 | logger.info(`schema diff result is ready`);
|
1001 | let conclusion = action.conclusion;
|
1002 | let annotations = action.annotations || [];
|
1003 | const changes = action.changes || [];
|
1004 | logger.info(`changes - ${changes.length}`);
|
1005 | logger.info(`annotations - ${changes.length}`);
|
1006 | const summary = createSummary(changes);
|
1007 | const approveLabelName = config.diff.approveLabel || 'approved-breaking-change';
|
1008 | const hasApprovedBreakingChangeLabel = pullRequestNumber
|
1009 | ? (_a = pullRequests[0].labels) === null || _a === void 0 ? void 0 : _a.find((label) => label.name === approveLabelName) : false;
|
1010 |
|
1011 | if (config.diff.failOnBreaking === false ||
|
1012 | hasApprovedBreakingChangeLabel) {
|
1013 | logger.info('FailOnBreaking disabled. Forcing SUCCESS');
|
1014 | conclusion = CheckConclusion.Success;
|
1015 | }
|
1016 | const title = conclusion === CheckConclusion.Failure
|
1017 | ? 'Something is wrong with your schema'
|
1018 | : 'Everything looks good';
|
1019 | if (config.diff.annotations === false) {
|
1020 | logger.info(`Anotations are disabled. Skipping annotations...`);
|
1021 | annotations = [];
|
1022 | }
|
1023 | else {
|
1024 | logger.info(`Sending annotations (${annotations.length})`);
|
1025 | }
|
1026 | yield annotate({
|
1027 | url: checkUrl,
|
1028 | context,
|
1029 | title,
|
1030 | summary,
|
1031 | annotations,
|
1032 | logger,
|
1033 | });
|
1034 | logger.info(`Finishing check (${conclusion})`);
|
1035 | yield complete({
|
1036 | url: checkUrl,
|
1037 | context,
|
1038 | conclusion,
|
1039 | logger,
|
1040 | });
|
1041 | logger.info(`done`);
|
1042 | }
|
1043 | catch (error) {
|
1044 | logger.error(error);
|
1045 | yield annotate({
|
1046 | url: checkUrl,
|
1047 | context,
|
1048 | title: `Failed to complete schema check`,
|
1049 | summary: `ERROR: ${error.message || error}`,
|
1050 | annotations: [],
|
1051 | logger,
|
1052 | });
|
1053 | yield complete({
|
1054 | url: checkUrl,
|
1055 | context,
|
1056 | conclusion: CheckConclusion.Failure,
|
1057 | logger,
|
1058 | });
|
1059 | }
|
1060 | });
|
1061 | }
|
1062 |
|
1063 | const allowedCheckActions = ['requested', 'rerequested', 'gh-action'];
|
1064 | function handleProbot(app) {
|
1065 | app.on('check_run', (context) => __awaiter(this, void 0, void 0, function* () {
|
1066 | const ref = context.payload.check_run.head_sha;
|
1067 | const { owner, repo } = context.repo();
|
1068 | const action = context.payload.action;
|
1069 | const pullRequests = context.payload.check_run.pull_requests;
|
1070 | const before = context.payload.check_run.check_suite.before;
|
1071 | if (allowedCheckActions.includes(action) === false) {
|
1072 | return;
|
1073 | }
|
1074 | const loadFile = createFileLoader({ context, owner, repo });
|
1075 | const loadConfig = createConfigLoader({ context, owner, repo, ref }, loadFile);
|
1076 | yield handleSchemaDiff({
|
1077 | action: 'check_run.' + action,
|
1078 | context,
|
1079 | ref,
|
1080 | repo,
|
1081 | owner,
|
1082 | loadFile,
|
1083 | loadConfig,
|
1084 | before,
|
1085 | pullRequests,
|
1086 | });
|
1087 | }));
|
1088 | app.on('check_suite', (context) => __awaiter(this, void 0, void 0, function* () {
|
1089 | const ref = context.payload.check_suite.head_sha;
|
1090 | const { owner, repo } = context.repo();
|
1091 | const action = context.payload.action;
|
1092 | const pullRequests = context.payload.check_suite.pull_requests;
|
1093 | const before = context.payload.check_suite.before;
|
1094 | if (allowedCheckActions.includes(action) === false) {
|
1095 | return;
|
1096 | }
|
1097 | const loadFile = createFileLoader({ context, owner, repo });
|
1098 | const loadConfig = createConfigLoader({ context, owner, repo, ref }, loadFile);
|
1099 | yield handleSchemaDiff({
|
1100 | action: 'check_suite.' + action,
|
1101 | context,
|
1102 | ref,
|
1103 | repo,
|
1104 | owner,
|
1105 | loadFile,
|
1106 | loadConfig,
|
1107 | before,
|
1108 | pullRequests,
|
1109 | });
|
1110 | }));
|
1111 | app.on('pull_request', (context) => __awaiter(this, void 0, void 0, function* () {
|
1112 | const ref = context.payload.pull_request.head.ref;
|
1113 | const pullRequestNumber = context.payload.pull_request.number;
|
1114 | const { owner, repo } = context.repo();
|
1115 | const action = context.payload.action;
|
1116 | const pullRequests = [context.payload.pull_request];
|
1117 | const before = context.payload.pull_request.base.ref;
|
1118 | if (['opened', 'synchronize', 'edited', 'labeled', 'unlabeled'].includes(action) === false) {
|
1119 | return;
|
1120 | }
|
1121 | const loadFile = createFileLoader({ context, owner, repo });
|
1122 | const loadConfig = createConfigLoader({ context, owner, repo, ref }, loadFile);
|
1123 | yield handleSchemaDiff({
|
1124 | action: 'pull_request.' + action,
|
1125 | context,
|
1126 | ref,
|
1127 | repo,
|
1128 | owner,
|
1129 | loadFile,
|
1130 | loadConfig,
|
1131 | before,
|
1132 | pullRequests,
|
1133 | pullRequestNumber,
|
1134 | });
|
1135 | }));
|
1136 | app.on('push', (context) => __awaiter(this, void 0, void 0, function* () {
|
1137 | const ref = context.payload.ref;
|
1138 | const { owner, repo } = context.repo();
|
1139 | const before = context.payload.before;
|
1140 | const loadFile = createFileLoader({ context, owner, repo });
|
1141 | const loadConfig = createConfigLoader({ context, owner, repo, ref }, loadFile);
|
1142 | yield handleSchemaChangeNotifications({
|
1143 | context,
|
1144 | ref,
|
1145 | before,
|
1146 | repo,
|
1147 | owner,
|
1148 | loadFile,
|
1149 | loadConfig,
|
1150 | });
|
1151 | }));
|
1152 | }
|
1153 |
|
1154 | export default handleProbot;
|
1155 | export { CheckConclusion, handleProbot as app, createConfig, createFileLoader, createSummary, diff, printSchemaFromEndpoint, quotesTransformer };
|
1156 |
|