@graphql-inspector/action 0.0.0-canary.35db993 → 0.0.0-canary.4c9888f

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/index.js ADDED
@@ -0,0 +1,423 @@
1
+ 'use strict';
2
+
3
+ const core = require('@actions/core');
4
+ const tslib = require('tslib');
5
+ const github = require('@graphql-inspector/github');
6
+ const fs = require('fs');
7
+ const path = require('path');
8
+ const child_process = require('child_process');
9
+ const github$1 = require('@actions/github');
10
+
11
+ const MAX_ARRAY_LENGTH = 10;
12
+ const MAX_RECURSIVE_DEPTH = 2;
13
+ /**
14
+ * Used to print values in error messages.
15
+ */
16
+
17
+ function inspect(value) {
18
+ return formatValue(value, []);
19
+ }
20
+
21
+ function formatValue(value, seenValues) {
22
+ switch (typeof value) {
23
+ case 'string':
24
+ return JSON.stringify(value);
25
+
26
+ case 'function':
27
+ return value.name ? `[function ${value.name}]` : '[function]';
28
+
29
+ case 'object':
30
+ return formatObjectValue(value, seenValues);
31
+
32
+ default:
33
+ return String(value);
34
+ }
35
+ }
36
+
37
+ function formatObjectValue(value, previouslySeenValues) {
38
+ if (value === null) {
39
+ return 'null';
40
+ }
41
+
42
+ if (previouslySeenValues.includes(value)) {
43
+ return '[Circular]';
44
+ }
45
+
46
+ const seenValues = [...previouslySeenValues, value];
47
+
48
+ if (isJSONable(value)) {
49
+ const jsonValue = value.toJSON(); // check for infinite recursion
50
+
51
+ if (jsonValue !== value) {
52
+ return typeof jsonValue === 'string'
53
+ ? jsonValue
54
+ : formatValue(jsonValue, seenValues);
55
+ }
56
+ } else if (Array.isArray(value)) {
57
+ return formatArray(value, seenValues);
58
+ }
59
+
60
+ return formatObject(value, seenValues);
61
+ }
62
+
63
+ function isJSONable(value) {
64
+ return typeof value.toJSON === 'function';
65
+ }
66
+
67
+ function formatObject(object, seenValues) {
68
+ const entries = Object.entries(object);
69
+
70
+ if (entries.length === 0) {
71
+ return '{}';
72
+ }
73
+
74
+ if (seenValues.length > MAX_RECURSIVE_DEPTH) {
75
+ return '[' + getObjectTag(object) + ']';
76
+ }
77
+
78
+ const properties = entries.map(
79
+ ([key, value]) => key + ': ' + formatValue(value, seenValues),
80
+ );
81
+ return '{ ' + properties.join(', ') + ' }';
82
+ }
83
+
84
+ function formatArray(array, seenValues) {
85
+ if (array.length === 0) {
86
+ return '[]';
87
+ }
88
+
89
+ if (seenValues.length > MAX_RECURSIVE_DEPTH) {
90
+ return '[Array]';
91
+ }
92
+
93
+ const len = Math.min(MAX_ARRAY_LENGTH, array.length);
94
+ const remaining = array.length - len;
95
+ const items = [];
96
+
97
+ for (let i = 0; i < len; ++i) {
98
+ items.push(formatValue(array[i], seenValues));
99
+ }
100
+
101
+ if (remaining === 1) {
102
+ items.push('... 1 more item');
103
+ } else if (remaining > 1) {
104
+ items.push(`... ${remaining} more items`);
105
+ }
106
+
107
+ return '[' + items.join(', ') + ']';
108
+ }
109
+
110
+ function getObjectTag(object) {
111
+ const tag = Object.prototype.toString
112
+ .call(object)
113
+ .replace(/^\[object /, '')
114
+ .replace(/]$/, '');
115
+
116
+ if (tag === 'Object' && typeof object.constructor === 'function') {
117
+ const name = object.constructor.name;
118
+
119
+ if (typeof name === 'string' && name !== '') {
120
+ return name;
121
+ }
122
+ }
123
+
124
+ return tag;
125
+ }
126
+
127
+ function devAssert(condition, message) {
128
+ const booleanCondition = Boolean(condition); // istanbul ignore else (See transformation done in './resources/inlineInvariant.js')
129
+
130
+ if (!booleanCondition) {
131
+ throw new Error(message);
132
+ }
133
+ }
134
+
135
+ /**
136
+ * A representation of source input to GraphQL. The `name` and `locationOffset` parameters are
137
+ * optional, but they are useful for clients who store GraphQL documents in source files.
138
+ * For example, if the GraphQL input starts at line 40 in a file named `Foo.graphql`, it might
139
+ * be useful for `name` to be `"Foo.graphql"` and location to be `{ line: 40, column: 1 }`.
140
+ * The `line` and `column` properties in `locationOffset` are 1-indexed.
141
+ */
142
+ class Source {
143
+ constructor(
144
+ body,
145
+ name = 'GraphQL request',
146
+ locationOffset = {
147
+ line: 1,
148
+ column: 1,
149
+ },
150
+ ) {
151
+ typeof body === 'string' ||
152
+ devAssert(false, `Body must be a string. Received: ${inspect(body)}.`);
153
+ this.body = body;
154
+ this.name = name;
155
+ this.locationOffset = locationOffset;
156
+ this.locationOffset.line > 0 ||
157
+ devAssert(
158
+ false,
159
+ 'line in locationOffset is 1-indexed and must be positive.',
160
+ );
161
+ this.locationOffset.column > 0 ||
162
+ devAssert(
163
+ false,
164
+ 'column in locationOffset is 1-indexed and must be positive.',
165
+ );
166
+ }
167
+
168
+ get [Symbol.toStringTag]() {
169
+ return 'Source';
170
+ }
171
+ }
172
+
173
+ function batch(items, limit) {
174
+ const batches = [];
175
+ const batchesNum = Math.ceil(items.length / limit);
176
+ // We still want to update check-run and send empty annotations
177
+ if (batchesNum === 0) {
178
+ return [[]];
179
+ }
180
+ for (let i = 0; i < batchesNum; i++) {
181
+ const start = i * limit;
182
+ const end = start + limit;
183
+ batches.push(items.slice(start, end));
184
+ }
185
+ return batches;
186
+ }
187
+
188
+ const CHECK_NAME = 'GraphQL Inspector';
189
+ function getCurrentCommitSha() {
190
+ const sha = child_process.execSync(`git rev-parse HEAD`).toString().trim();
191
+ try {
192
+ const msg = child_process.execSync(`git show ${sha} -s --format=%s`).toString().trim();
193
+ const PR_MSG = /Merge (\w+) into \w+/i;
194
+ if (PR_MSG.test(msg)) {
195
+ const result = PR_MSG.exec(msg);
196
+ if (result) {
197
+ return result[1];
198
+ }
199
+ }
200
+ }
201
+ catch (e) {
202
+ //
203
+ }
204
+ return sha;
205
+ }
206
+ function run() {
207
+ var _a, _b, _c;
208
+ return tslib.__awaiter(this, void 0, void 0, function* () {
209
+ core.info(`GraphQL Inspector started`);
210
+ // env
211
+ let ref = process.env.GITHUB_SHA;
212
+ const commitSha = getCurrentCommitSha();
213
+ core.info(`Ref: ${ref}`);
214
+ core.info(`Commit SHA: ${commitSha}`);
215
+ const token = core.getInput('github-token', { required: true });
216
+ const checkName = core.getInput('name') || CHECK_NAME;
217
+ let workspace = process.env.GITHUB_WORKSPACE;
218
+ if (!workspace) {
219
+ return core.setFailed('Failed to resolve workspace directory. GITHUB_WORKSPACE is missing');
220
+ }
221
+ const useMerge = castToBoolean(core.getInput('experimental_merge'), true);
222
+ const useAnnotations = castToBoolean(core.getInput('annotations'));
223
+ const failOnBreaking = castToBoolean(core.getInput('fail-on-breaking'));
224
+ const endpoint = core.getInput('endpoint');
225
+ const approveLabel = core.getInput('approve-label') || 'approved-breaking-change';
226
+ const octokit = github$1.getOctokit(token);
227
+ // repo
228
+ const { owner, repo } = github$1.context.repo;
229
+ core.info(`Creating a check named "${checkName}"`);
230
+ const check = yield octokit.checks.create({
231
+ owner,
232
+ repo,
233
+ name: checkName,
234
+ head_sha: commitSha,
235
+ status: 'in_progress',
236
+ });
237
+ const checkId = check.data.id;
238
+ core.info(`Check ID: ${checkId}`);
239
+ const schemaPointer = core.getInput('schema', { required: true });
240
+ const loadFile = fileLoader({
241
+ octokit,
242
+ owner,
243
+ repo,
244
+ });
245
+ if (!schemaPointer) {
246
+ core.error('No `schema` variable');
247
+ return core.setFailed('Failed to find `schema` variable');
248
+ }
249
+ let [schemaRef, schemaPath] = schemaPointer.split(':');
250
+ if (useMerge && github$1.context.payload.pull_request) {
251
+ ref = `refs/pull/${github$1.context.payload.pull_request.number}/merge`;
252
+ workspace = undefined;
253
+ core.info(`EXPERIMENTAL - Using Pull Request ${ref}`);
254
+ const baseRef = (_b = (_a = github$1.context.payload.pull_request) === null || _a === void 0 ? void 0 : _a.base) === null || _b === void 0 ? void 0 : _b.ref;
255
+ if (baseRef) {
256
+ schemaRef = baseRef;
257
+ core.info(`EXPERIMENTAL - Using ${baseRef} as base schema ref`);
258
+ }
259
+ }
260
+ if (endpoint) {
261
+ schemaPath = schemaPointer;
262
+ }
263
+ const isNewSchemaUrl = endpoint && schemaPath.startsWith('http');
264
+ const [oldFile, newFile] = yield Promise.all([
265
+ endpoint
266
+ ? github.printSchemaFromEndpoint(endpoint)
267
+ : loadFile({
268
+ ref: schemaRef,
269
+ path: schemaPath,
270
+ }),
271
+ isNewSchemaUrl
272
+ ? github.printSchemaFromEndpoint(schemaPath)
273
+ : loadFile({
274
+ path: schemaPath,
275
+ ref,
276
+ workspace,
277
+ }),
278
+ ]);
279
+ core.info('Got both sources');
280
+ const sources = {
281
+ old: new Source(oldFile, endpoint || `${schemaRef}:${schemaPath}`),
282
+ new: new Source(newFile, schemaPath),
283
+ };
284
+ const schemas = {
285
+ old: github.produceSchema(sources.old),
286
+ new: github.produceSchema(sources.new),
287
+ };
288
+ core.info(`Built both schemas`);
289
+ core.info(`Start comparing schemas`);
290
+ const action = yield github.diff({
291
+ path: schemaPath,
292
+ schemas,
293
+ sources,
294
+ });
295
+ let conclusion = action.conclusion;
296
+ let annotations = action.annotations || [];
297
+ const changes = action.changes || [];
298
+ core.setOutput('changes', `${changes.length || 0}`);
299
+ core.info(`Changes: ${changes.length || 0}`);
300
+ const hasApprovedBreakingChangeLabel = github$1.context.payload.pull_request
301
+ ? (_c = github$1.context.payload.pull_request.labels) === null || _c === void 0 ? void 0 : _c.some((label) => label.name === approveLabel)
302
+ : false;
303
+ // Force Success when failOnBreaking is disabled
304
+ if ((failOnBreaking === false || hasApprovedBreakingChangeLabel) && conclusion === github.CheckConclusion.Failure) {
305
+ core.info('FailOnBreaking disabled. Forcing SUCCESS');
306
+ conclusion = github.CheckConclusion.Success;
307
+ }
308
+ if (useAnnotations === false || isNewSchemaUrl) {
309
+ core.info(`Anotations are disabled. Skipping annotations...`);
310
+ annotations = [];
311
+ }
312
+ const summary = github.createSummary(changes, 100, false);
313
+ const title = conclusion === github.CheckConclusion.Failure
314
+ ? 'Something is wrong with your schema'
315
+ : 'Everything looks good';
316
+ core.info(`Conclusion: ${conclusion}`);
317
+ try {
318
+ return yield updateCheckRun(octokit, checkId, {
319
+ conclusion,
320
+ output: { title, summary, annotations },
321
+ });
322
+ }
323
+ catch (e) {
324
+ // Error
325
+ core.error(e.message || e);
326
+ const title = 'Invalid config. Failed to add annotation';
327
+ yield updateCheckRun(octokit, checkId, {
328
+ conclusion: github.CheckConclusion.Failure,
329
+ output: { title, summary: title, annotations: [] },
330
+ });
331
+ return core.setFailed(title);
332
+ }
333
+ });
334
+ }
335
+ function fileLoader({ octokit, owner, repo, }) {
336
+ const query = /* GraphQL */ `
337
+ query GetFile($repo: String!, $owner: String!, $expression: String!) {
338
+ repository(name: $repo, owner: $owner) {
339
+ object(expression: $expression) {
340
+ ... on Blob {
341
+ text
342
+ }
343
+ }
344
+ }
345
+ }
346
+ `;
347
+ return function loadFile(file) {
348
+ var _a, _b;
349
+ return tslib.__awaiter(this, void 0, void 0, function* () {
350
+ if (file.workspace) {
351
+ return fs.readFileSync(path.resolve(file.workspace, file.path), {
352
+ encoding: 'utf-8',
353
+ });
354
+ }
355
+ const result = yield octokit.graphql(query, {
356
+ repo,
357
+ owner,
358
+ expression: `${file.ref}:${file.path}`,
359
+ });
360
+ core.info(`Query ${file.ref}:${file.path} from ${owner}/${repo}`);
361
+ try {
362
+ if ((_b = (_a = result === null || result === void 0 ? void 0 : result.repository) === null || _a === void 0 ? void 0 : _a.object) === null || _b === void 0 ? void 0 : _b.text) {
363
+ return result.repository.object.text;
364
+ }
365
+ throw new Error('result.repository.object.text is null');
366
+ }
367
+ catch (error) {
368
+ console.log(result);
369
+ console.error(error);
370
+ throw new Error(`Failed to load '${file.path}' (ref: ${file.ref})`);
371
+ }
372
+ });
373
+ };
374
+ }
375
+ function updateCheckRun(octokit, checkId, { conclusion, output }) {
376
+ return tslib.__awaiter(this, void 0, void 0, function* () {
377
+ core.info(`Updating check: ${checkId}`);
378
+ const { title, summary, annotations = [] } = output;
379
+ const batches = batch(annotations, 50);
380
+ core.info(`annotations to be sent: ${annotations.length}`);
381
+ yield octokit.checks.update(Object.assign(Object.assign({ check_run_id: checkId, completed_at: new Date().toISOString(), status: 'completed' }, github$1.context.repo), { conclusion, output: {
382
+ title, summary
383
+ } }));
384
+ try {
385
+ yield Promise.all(batches.map((chunk) => tslib.__awaiter(this, void 0, void 0, function* () {
386
+ yield octokit.checks.update(Object.assign(Object.assign({ check_run_id: checkId }, github$1.context.repo), { output: {
387
+ title,
388
+ summary,
389
+ annotations: chunk,
390
+ } }));
391
+ core.info(`annotations sent (${chunk.length})`);
392
+ })));
393
+ }
394
+ catch (error) {
395
+ core.error(`failed to send annotations: ${error}`);
396
+ throw error;
397
+ }
398
+ // Fail
399
+ if (conclusion === github.CheckConclusion.Failure) {
400
+ return core.setFailed(output.title);
401
+ }
402
+ // Success or Neutral
403
+ });
404
+ }
405
+ /**
406
+ * Treats non-falsy value as true
407
+ */
408
+ function castToBoolean(value, defaultValue) {
409
+ if (typeof value === 'boolean') {
410
+ return value;
411
+ }
412
+ if (value === 'true' || value === 'false') {
413
+ return value === 'true';
414
+ }
415
+ if (typeof defaultValue === 'boolean') {
416
+ return defaultValue;
417
+ }
418
+ return true;
419
+ }
420
+
421
+ run().catch((e) => {
422
+ core.setFailed(e.message || e);
423
+ });