UNPKG

41.4 kBJavaScriptView Raw
1import { __awaiter } from 'tslib';
2import Dataloader from 'dataloader';
3import yaml from 'js-yaml';
4import axios from 'axios';
5import { getIntrospectionQuery, printSchema, buildClientSchema, Source, buildSchema, parse, Kind, getLocation } from 'graphql';
6import { CriticalityLevel, diff as diff$1 } from '@graphql-inspector/core';
7
8function bolderize(msg) {
9 return quotesTransformer(msg, '**');
10}
11function 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}
21function slackCoderize(msg) {
22 return quotesTransformer(msg, '`');
23}
24function discordCoderize(msg) {
25 return quotesTransformer(msg, '`');
26}
27function filterChangesByLevel(level) {
28 return (change) => change.criticality.level === level;
29}
30function 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}
61function isNil(val) {
62 return !val && typeof val !== 'boolean';
63}
64function 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}
77function batch(items, limit) {
78 const batches = [];
79 const batchesNum = Math.ceil(items.length / limit);
80 // We still want to update check-run and send empty annotations
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}
91function objectFromEntries(iterable) {
92 return [...iterable].reduce((obj, [key, val]) => {
93 obj[key] = val;
94 return obj;
95 }, {});
96}
97
98function 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 /* GraphQL */ `
114 query GetFile($repo: String!, $owner: String!, ${variables}) {
115 repository(name: $repo, owner: $owner) {
116 ${files}
117 }
118 }
119 `.replace(/\s+/g, ' ');
120}
121function 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}
161function 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}
195function 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}
212function loadSources({ config, oldPointer, newPointer, loadFile, }) {
213 return __awaiter(this, void 0, void 0, function* () {
214 // Here, config.endpoint is defined only if target's branch matches branch of environment
215 // otherwise it's empty
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
230const defaultConfigName = '__default';
231const defaultFallbackBranch = '*';
232const diffDefault = {
233 annotations: true,
234 failOnBreaking: true,
235};
236const notificationsDefault = false;
237function 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}
284function 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}
294function 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}
317function isNormalizedLegacyConfig(config) {
318 return typeof config[defaultConfigName] === 'object';
319}
320function isLegacyConfig(config) {
321 return config.schema && typeof config.schema === 'object';
322}
323function isSingleEnvironmentConfig(config) {
324 return !config.env;
325}
326function isMultipleEnvironmentConfig(config) {
327 return !isLegacyConfig(config) && !isSingleEnvironmentConfig(config);
328}
329function 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// I'm not very proud of it :)
346function 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
364function 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}
382function 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}
399function 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}
416function 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}
444function 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}
456function 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}
484function 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}
494function pluralize(word, num) {
495 return word + (num > 1 ? 's' : '');
496}
497
498function 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
520function 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}
613function hasNotificationsEnabled(notifications) {
614 return notifications && typeof notifications === 'object';
615}
616
617var AnnotationLevel;
618(function (AnnotationLevel) {
619 AnnotationLevel["Failure"] = "failure";
620 AnnotationLevel["Warning"] = "warning";
621 AnnotationLevel["Notice"] = "notice";
622})(AnnotationLevel || (AnnotationLevel = {}));
623var CheckStatus;
624(function (CheckStatus) {
625 CheckStatus["InProgress"] = "in_progress";
626 CheckStatus["Completed"] = "completed";
627})(CheckStatus || (CheckStatus = {}));
628var CheckConclusion;
629(function (CheckConclusion) {
630 CheckConclusion["Success"] = "success";
631 CheckConclusion["Neutral"] = "neutral";
632 CheckConclusion["Failure"] = "failure";
633})(CheckConclusion || (CheckConclusion = {}));
634
635const headers = { accept: 'application/vnd.github.antiope-preview+json' };
636function 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}
657function 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}
683function 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
703function 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}
748function resolveScalarTypeDefinitionNode(_path, definition) {
749 return definition;
750}
751function resolveUnionTypeDefinitionNode(_path, definition) {
752 return definition;
753}
754function 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}
759function 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}
772function resolveInterfaceTypeDefinition(path, definition) {
773 const [fieldName, argName] = path;
774 if (fieldName) {
775 return resolveFieldDefinition([fieldName, argName], definition);
776 }
777 return definition;
778}
779function resolveInputObjectTypeDefinition(path, definition) {
780 const [fieldName] = path;
781 if (fieldName) {
782 return resolveFieldDefinition([fieldName], definition);
783 }
784 return definition;
785}
786function 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}
796function resolveObjectTypeDefinition(path, definition) {
797 const [fieldName, argName] = path;
798 if (fieldName) {
799 return resolveFieldDefinition([fieldName, argName], definition);
800 }
801 return definition;
802}
803function 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}
813function 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
830function 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}
863const levelMap = {
864 [CriticalityLevel.Breaking]: AnnotationLevel.Failure,
865 [CriticalityLevel.Dangerous]: AnnotationLevel.Warning,
866 [CriticalityLevel.NonBreaking]: AnnotationLevel.Notice,
867};
868function 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}
882function 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
894class 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
904function 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 // on non-environment related PRs, use a branch from first associated pull request
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 // Force Success when failOnBreaking is disabled
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
1063const allowedCheckActions = ['requested', 'rerequested', 'gh-action'];
1064function 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
1154export default handleProbot;
1155export { CheckConclusion, handleProbot as app, createConfig, createFileLoader, createSummary, diff, printSchemaFromEndpoint, quotesTransformer };
1156//# sourceMappingURL=index.esm.js.map