@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 +423 -0
- package/index.mjs +421 -0
- package/package.json +14 -10
- package/utils.d.ts +1 -0
- package/index.cjs.js +0 -10212
- package/index.cjs.js.map +0 -1
- package/index.esm.js +0 -10210
- package/index.esm.js.map +0 -1
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
|
+
});
|