UNPKG

@graphql-hive/cli

Version:

A CLI util to manage and control your GraphQL Hive

465 lines • 17.8 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const tslib_1 = require("tslib"); const node_fs_1 = tslib_1.__importDefault(require("node:fs")); const core_1 = require("@oclif/core"); const base_command_1 = tslib_1.__importDefault(require("../../base-command")); const gql_1 = require("../../gql"); const config_1 = require("../../helpers/config"); const errors_1 = require("../../helpers/errors"); const git_1 = require("../../helpers/git"); const git_schema_loader_1 = require("../../helpers/git-schema-loader"); const schema_1 = require("../../helpers/schema"); const TargetInput = tslib_1.__importStar(require("../../helpers/target-input")); const approveFailedSchemaCheckMutation = (0, gql_1.graphql)(/* GraphQL */ ` mutation approveFailedSchemaCheck($input: ApproveFailedSchemaCheckInput!) { approveFailedSchemaCheck(input: $input) { ok { schemaCheck { id } } error { message } } } `); const schemaCheckMutation = (0, gql_1.graphql)(/* GraphQL */ ` mutation CLI_SchemaCheckMutation($input: SchemaCheckInput!) { schemaCheck(input: $input) { __typename ... on SchemaCheckSuccess { valid initial warnings { nodes { message source line column } total } changes { edges { __typename } ...RenderChanges_schemaChanges } schemaCheck { id webUrl } } ... on SchemaCheckError { valid changes { edges { __typename } ...RenderChanges_schemaChanges } warnings { nodes { message source line column } total } errors { ...RenderErrors_SchemaErrorConnectionFragment } schemaCheck { id webUrl } } ... on GitHubSchemaCheckSuccess { message } ... on GitHubSchemaCheckError { message } } } `); /** Only used with `--github`, so that servers without these fields keep working for other checks. */ const schemaCheckGitHubMutation = (0, gql_1.graphql)(/* GraphQL */ ` mutation CLI_SchemaCheckGitHubMutation($input: SchemaCheckInput!) { schemaCheck(input: $input) { __typename ... on SchemaCheckSuccess { valid initial warnings { nodes { message source line column } total } changes { edges { __typename } ...RenderChanges_schemaChanges } schemaCheck { id webUrl } } ... on SchemaCheckError { valid changes { edges { __typename } ...RenderChanges_schemaChanges } warnings { nodes { message source line column } total } errors { ...RenderErrors_SchemaErrorConnectionFragment } schemaCheck { id webUrl } } ... on GitHubSchemaCheckSuccess { message isValid schemaCheck { id webUrl } } ... on GitHubSchemaCheckError { message } } } `); class SchemaCheck extends base_command_1.default { async run() { var _a, _b, _c, _d, _e, _f, _g; try { const { flags, args } = await this.parse(SchemaCheck); await this.require(flags); let target = null; if (flags.target) { const result = TargetInput.parse(flags.target); if (result.type === 'error') { throw new errors_1.InvalidTargetError(); } target = result.data; } const service = flags.service; const forceSafe = flags.forceSafe; const usesGitHubApp = flags.github === true; let endpoint, accessToken; try { endpoint = this.ensure({ key: 'registry.endpoint', args: flags, legacyFlagName: 'registry', defaultValue: config_1.graphqlEndpoint, env: 'HIVE_REGISTRY', description: SchemaCheck.flags['registry.endpoint'].description, }); } catch (e) { throw e instanceof errors_1.MissingArgumentsError ? new errors_1.MissingEndpointError() : e; } const schemaPointer = args.file; try { accessToken = this.ensure({ key: 'registry.accessToken', args: flags, legacyFlagName: 'token', env: 'HIVE_TOKEN', description: SchemaCheck.flags['registry.accessToken'].description, }); } catch (e) { throw e instanceof errors_1.MissingArgumentsError ? new errors_1.MissingRegistryTokenError() : e; } const git = await (0, git_1.gitInfo)(() => { // noop }); const commit = flags.commit || (git === null || git === void 0 ? void 0 : git.commit); let github = null; if (usesGitHubApp) { if (!commit) { throw new errors_1.CommitRequiredError(); } if (!git.repository) { throw new errors_1.GithubRepositoryRequiredError(); } if (!git.pullRequestNumber) { this.warn("Could not resolve pull request number. Are you running this command on a 'pull_request' or 'merge_group' event?\n" + 'See https://the-guild.dev/graphql/hive/docs/other-integrations/ci-cd#github-workflow-for-ci'); } github = { commit: commit, repository: git.repository, pullRequestNumber: git.pullRequestNumber, }; } let minifiedBaselineSdl = null; let baselineSchemaHash = null; if (flags.baseline) { const baselinePointer = flags.baseline; const gitResult = (0, git_schema_loader_1.parseBaselineGitFileReference)(baselinePointer); const result = gitResult.status === 'error' ? await (0, schema_1.loadSchema)('first-federation-then-graphql-introspection', baselinePointer, { logger: this.logger, }) .catch(() => { throw new errors_1.SchemaFileNotFoundError(baselinePointer, 'Failed to retrieve the baseline schema from ' + baselinePointer); }) .then(sdl => ({ status: 'ok', sdl, })) : (0, git_schema_loader_1.loadSchemaFromGitHistory)(gitResult.filePath, gitResult.commit); if (result.status === 'error') { switch (result.error.type) { case 'git': throw new errors_1.SchemaFileNotFoundError(result.error.path, 'Failed to retrieve the baseline schema from the git history.\n' + result.error.message); case 'path': throw new errors_1.SchemaFileNotFoundError(schemaPointer, `When using the '--baseline' flag, the file path must point to a single file containing the schema SDL.`); } } minifiedBaselineSdl = (0, schema_1.minifySchema)(result.sdl); baselineSchemaHash = (_a = gitResult.commit) !== null && _a !== void 0 ? _a : git.baselineCommit; } const rawSdl = await (0, schema_1.loadSchemaSdl)(schemaPointer, { logger: this.logger }); const author = flags.author || (git === null || git === void 0 ? void 0 : git.author); const sdl = (0, schema_1.minifySchema)(rawSdl); const input = { service, sdl, github, meta: !!commit && !!author ? { commit, author, } : null, contextId: (_b = flags.contextId) !== null && _b !== void 0 ? _b : undefined, target, url: flags.url, schemaProposalId: flags.schemaProposalId, baseline: minifiedBaselineSdl ? { sdl: minifiedBaselineSdl, hash: baselineSchemaHash, } : null, }; const api = this.registryApi(endpoint, accessToken); /** Gateway timeout is 60 seconds. */ const timeout = 55000; const result = usesGitHubApp ? await api.request({ operation: schemaCheckGitHubMutation, variables: { input }, timeout, }) : await api.request({ operation: schemaCheckMutation, variables: { input }, timeout, }); const handleFailedCheck = async (schemaCheckId) => { if (!forceSafe) { throw new errors_1.SchemaCheckFailedError(); } if (!(target === null || target === void 0 ? void 0 : target.bySelector)) { throw new errors_1.MissingArgumentsError([ 'target', 'The target slug in the format "$organizationSlug/$projectSlug/$targetSlug". The `--forceSafe` flag requires the target as a slug, not as an ID.', ]); } if (!schemaCheckId) { throw new errors_1.SchemaCheckApprovalFailedError('The registry did not store this schema check, so it cannot be approved.'); } const approvalResult = await api.request({ operation: approveFailedSchemaCheckMutation, variables: { input: { organizationSlug: target.bySelector.organizationSlug, projectSlug: target.bySelector.projectSlug, targetSlug: target.bySelector.targetSlug, schemaCheckId, comment: 'Check force approved automatically via CLI --forceSafe flag', author: author !== null && author !== void 0 ? author : '', }, }, }); if (approvalResult.approveFailedSchemaCheck.error) { throw new errors_1.SchemaCheckApprovalFailedError(approvalResult.approveFailedSchemaCheck.error.message); } this.logSuccess('Breaking changes were expected (forced)'); }; if (flags.experimentalJsonFile) { node_fs_1.default.writeFileSync(flags.experimentalJsonFile, JSON.stringify(result, null, 2)); } const payload = result.schemaCheck; if (payload.__typename === 'SchemaCheckSuccess') { const changes = payload.changes; if (payload.initial) { this.logSuccess('Schema registry is empty, nothing to compare your schema with.'); } else if (!(changes === null || changes === void 0 ? void 0 : changes.edges.length)) { this.logSuccess('No changes'); } else { this.log((0, schema_1.renderChanges)(changes)); } const warnings = payload.warnings; if (warnings === null || warnings === void 0 ? void 0 : warnings.total) { this.log((0, schema_1.renderWarnings)(warnings)); } if ((_c = payload.schemaCheck) === null || _c === void 0 ? void 0 : _c.webUrl) { this.log(`View full report:\n${payload.schemaCheck.webUrl}`); } } else if (payload.__typename === 'SchemaCheckError') { const changes = payload.changes; const errors = payload.errors; const warnings = payload.warnings; this.log((0, schema_1.renderErrors)(errors)); if (warnings === null || warnings === void 0 ? void 0 : warnings.total) { this.log((0, schema_1.renderWarnings)(warnings)); } if (changes === null || changes === void 0 ? void 0 : changes.edges.length) { this.log((0, schema_1.renderChanges)(changes)); } if ((_d = payload.schemaCheck) === null || _d === void 0 ? void 0 : _d.webUrl) { this.log(`View full report:\n${payload.schemaCheck.webUrl}`); } this.log(''); await handleFailedCheck((_e = payload.schemaCheck) === null || _e === void 0 ? void 0 : _e.id); } else if (payload.__typename === 'GitHubSchemaCheckSuccess') { const gitHubResult = payload; if (gitHubResult.isValid) { this.logSuccess(gitHubResult.message); } if ((_f = gitHubResult.schemaCheck) === null || _f === void 0 ? void 0 : _f.webUrl) { this.log(`View full report:\n${gitHubResult.schemaCheck.webUrl}`); } if (!gitHubResult.isValid) { await handleFailedCheck((_g = gitHubResult.schemaCheck) === null || _g === void 0 ? void 0 : _g.id); } } else { throw new errors_1.APIError(payload.message); } } catch (error) { if (error instanceof core_1.Errors.CLIError) { throw error; } else { this.logFailure('Failed to check schema'); throw new errors_1.UnexpectedError(error); } } } } SchemaCheck.description = 'checks schema'; SchemaCheck.flags = { service: core_1.Flags.string({ description: 'service name (only for distributed schemas)', }), 'registry.endpoint': core_1.Flags.string({ description: 'registry endpoint', }), /** @deprecated */ registry: core_1.Flags.string({ description: 'registry address', deprecated: { message: 'use --registry.endpoint instead', version: '0.21.0', }, }), 'registry.accessToken': core_1.Flags.string({ description: 'registry access token', }), /** @deprecated */ token: core_1.Flags.string({ description: 'api token', deprecated: { message: 'use --registry.accessToken instead', version: '0.21.0', }, }), experimentalJsonFile: core_1.Flags.string({ name: 'experimental-json-file', description: "File path to output a JSON file containing the command's result. Useful for e.g. CI scripting with `jq`.", }), forceSafe: core_1.Flags.boolean({ description: 'mark the check as safe, breaking changes are expected', }), github: core_1.Flags.boolean({ description: 'Connect with GitHub Application', default: false, }), require: core_1.Flags.string({ description: 'Loads specific require.extensions before running the codegen and reading the configuration', default: [], multiple: true, }), author: core_1.Flags.string({ description: 'Author of the change', }), commit: core_1.Flags.string({ description: 'Associated commit sha', }), baseline: core_1.Flags.string({ description: 'File containing the schema before the current change.\n' + 'Baseline schema to compare against. Accepts a local file path or a file at a' + 'Git revision using `<revision>:<path>`.', }), contextId: core_1.Flags.string({ description: 'Context ID for grouping the schema check.', }), target: core_1.Flags.string({ description: 'The target against which to check the schema (slug or ID).' + ' This can either be a slug following the format "$organizationSlug/$projectSlug/$targetSlug" (e.g "the-guild/graphql-hive/staging")' + ' or an UUID (e.g. "a0f4c605-6541-4350-8cfe-b31f21a4bf80").', }), url: core_1.Flags.string({ description: 'If checking a service, then you can optionally provide the service URL to see the difference in the supergraph during the check.', }), schemaProposalId: core_1.Flags.string({ description: 'Attach the schema check to a schema proposal.', }), }; SchemaCheck.args = { file: core_1.Args.string({ name: 'file', required: true, description: 'Path to the schema file(s)', hidden: false, }), }; exports.default = SchemaCheck; //# sourceMappingURL=check.js.map