From 4d610749d6b14ccae54e1538bcc339e93da3e02f Mon Sep 17 00:00:00 2001 From: Peter-Phi-Tran Date: Tue, 16 Jun 2026 17:08:56 -0500 Subject: [PATCH] feat(cli): add JSON Schema for hive.json config file --- .changeset/cli-hive-json-schema.md | 20 ++++ packages/libraries/cli/README.md | 4 + .../libraries/cli/hive-config.schema.json | 94 +++++++++++++++++++ packages/libraries/cli/hive.json | 1 + packages/libraries/cli/package.json | 2 + packages/libraries/cli/src/helpers/config.ts | 2 + .../cli/tests/hive-config.schema.spec.ts | 73 ++++++++++++++ pnpm-lock.yaml | 3 + 8 files changed, 199 insertions(+) create mode 100644 .changeset/cli-hive-json-schema.md create mode 100644 packages/libraries/cli/hive-config.schema.json create mode 100644 packages/libraries/cli/tests/hive-config.schema.spec.ts diff --git a/.changeset/cli-hive-json-schema.md b/.changeset/cli-hive-json-schema.md new file mode 100644 index 00000000000..6123073b054 --- /dev/null +++ b/.changeset/cli-hive-json-schema.md @@ -0,0 +1,20 @@ +--- +'@graphql-hive/cli': minor +--- + +Add a JSON Schema for the `hive.json` configuration file. + +The CLI now ships a `hive-config.schema.json` describing the supported `hive.json` shapes (the modern +`registry`/`cdn` format, the deprecated flat format, and namespaced/space configurations). Reference +it from your config to get autocompletion, inline documentation, and validation in editors that +support JSON Schema: + +```json +{ + "$schema": "https://raw.githubusercontent.com/graphql-hive/console/main/packages/libraries/cli/hive-config.schema.json", + "registry": { + "endpoint": "", + "accessToken": "" + } +} +``` diff --git a/packages/libraries/cli/README.md b/packages/libraries/cli/README.md index 83b1eed7afa..c93e45832a9 100644 --- a/packages/libraries/cli/README.md +++ b/packages/libraries/cli/README.md @@ -584,6 +584,7 @@ This is how the structure of the config file should look like: ```json { + "$schema": "https://raw.githubusercontent.com/graphql-hive/console/main/packages/libraries/cli/hive-config.schema.json", "registry": { "endpoint": "", "accessToken": "" @@ -594,3 +595,6 @@ This is how the structure of the config file should look like: } } ``` + +The optional `$schema` property enables autocompletion, inline documentation, and validation for +`hive.json` in editors that support JSON Schema (such as VS Code). diff --git a/packages/libraries/cli/hive-config.schema.json b/packages/libraries/cli/hive-config.schema.json new file mode 100644 index 00000000000..99abbc6df6a --- /dev/null +++ b/packages/libraries/cli/hive-config.schema.json @@ -0,0 +1,94 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://raw.githubusercontent.com/graphql-hive/console/main/packages/libraries/cli/hive-config.schema.json", + "title": "GraphQL Hive CLI configuration (hive.json)", + "description": "Configuration file for the GraphQL Hive CLI (`hive`). Configuration is resolved with the priority: CLI arguments > environment variables > hive.json. See https://the-guild.dev/graphql/hive/docs/api-reference/cli for details.", + "$comment": "Keep this schema in sync with ConfigModel and LegacyConfigModel in packages/libraries/cli/src/helpers/config.ts.", + "anyOf": [ + { "$ref": "#/definitions/modernConfig" }, + { "$ref": "#/definitions/legacyConfig" }, + { "$ref": "#/definitions/namespacedConfig" } + ], + "definitions": { + "registry": { + "type": "object", + "additionalProperties": false, + "description": "Schema registry connection settings.", + "properties": { + "endpoint": { + "type": "string", + "format": "uri", + "description": "GraphQL endpoint of the Hive schema registry. Defaults to https://app.graphql-hive.com/graphql. Set this when self-hosting Hive. Can also be provided via the HIVE_REGISTRY environment variable or the --registry.endpoint argument." + }, + "accessToken": { + "type": "string", + "description": "Registry access token used to authenticate registry commands such as schema:publish and schema:check. Can also be provided via the HIVE_TOKEN environment variable or the --registry.accessToken argument." + } + } + }, + "cdn": { + "type": "object", + "additionalProperties": false, + "description": "High-availability CDN settings used by artifact:fetch.", + "properties": { + "endpoint": { + "type": "string", + "format": "uri", + "description": "High-availability CDN endpoint for fetching artifacts. Can also be provided via the HIVE_CDN_ENDPOINT environment variable or the --cdn.endpoint argument." + }, + "accessToken": { + "type": "string", + "description": "CDN access token used by artifact:fetch. Can also be provided via the --cdn.accessToken argument." + } + } + }, + "modernConfig": { + "type": "object", + "additionalProperties": false, + "description": "Hive CLI configuration.", + "properties": { + "$schema": { + "type": "string", + "description": "URL of the JSON Schema used to validate this file." + }, + "registry": { "$ref": "#/definitions/registry" }, + "cdn": { "$ref": "#/definitions/cdn" } + } + }, + "legacyConfig": { + "type": "object", + "additionalProperties": false, + "description": "Deprecated flat configuration format. Use the `registry` object (modernConfig) instead.", + "properties": { + "$schema": { + "type": "string", + "description": "URL of the JSON Schema used to validate this file." + }, + "registry": { + "type": "string", + "description": "Deprecated. Registry endpoint URL. Use `registry.endpoint` instead." + }, + "token": { + "type": "string", + "description": "Deprecated. Registry access token. Use `registry.accessToken` instead." + } + } + }, + "namespacedConfig": { + "type": "object", + "description": "Multiple named configurations ('spaces'). The active space is selected with the HIVE_SPACE environment variable, falling back to the `default` space. Each value is a Hive CLI configuration.", + "properties": { + "$schema": { + "type": "string", + "description": "URL of the JSON Schema used to validate this file." + } + }, + "additionalProperties": { + "anyOf": [ + { "$ref": "#/definitions/modernConfig" }, + { "$ref": "#/definitions/legacyConfig" } + ] + } + } + } +} diff --git a/packages/libraries/cli/hive.json b/packages/libraries/cli/hive.json index 6630d30fac4..61d6d4a9020 100644 --- a/packages/libraries/cli/hive.json +++ b/packages/libraries/cli/hive.json @@ -1,4 +1,5 @@ { + "$schema": "https://raw.githubusercontent.com/graphql-hive/console/main/packages/libraries/cli/hive-config.schema.json", "registry": { "endpoint": "http://localhost:3001/graphql", "accessToken": "905818792799ac1194c1d69d64c94a1a" diff --git a/packages/libraries/cli/package.json b/packages/libraries/cli/package.json index c2ca87c5829..9897c179eba 100644 --- a/packages/libraries/cli/package.json +++ b/packages/libraries/cli/package.json @@ -24,6 +24,7 @@ "files": [ "/bin", "/dist", + "/hive-config.schema.json", "/oclif.manifest.json" ], "keywords": [ @@ -71,6 +72,7 @@ }, "devDependencies": { "@types/env-ci": "3.1.4", + "ajv": "8.18.0", "graphql": "16.9.0", "oclif": "4.22.65", "rimraf": "6.1.3", diff --git a/packages/libraries/cli/src/helpers/config.ts b/packages/libraries/cli/src/helpers/config.ts index 12d55be5632..148e8cfec2b 100644 --- a/packages/libraries/cli/src/helpers/config.ts +++ b/packages/libraries/cli/src/helpers/config.ts @@ -2,6 +2,8 @@ import fs from 'fs'; import path from 'path'; import { z } from 'zod'; +// NOTE: When changing these models, update the public JSON Schema at +// packages/libraries/cli/hive-config.schema.json (and the SchemaStore catalog entry) to match. const LegacyConfigModel = z.object({ registry: z.string().optional(), token: z.string().optional(), diff --git a/packages/libraries/cli/tests/hive-config.schema.spec.ts b/packages/libraries/cli/tests/hive-config.schema.spec.ts new file mode 100644 index 00000000000..9ef8000a358 --- /dev/null +++ b/packages/libraries/cli/tests/hive-config.schema.spec.ts @@ -0,0 +1,73 @@ +import { readFileSync } from 'node:fs'; +import { join } from 'node:path'; +import Ajv from 'ajv'; + +// Compiles the shipped JSON Schema and asserts it accepts every `hive.json` shape +// the `Config` class accepts and rejects common typos. Keep the fixtures in sync +// with ConfigModel / LegacyConfigModel in src/helpers/config.ts. +const schema = JSON.parse( + readFileSync(join(__dirname, '..', 'hive-config.schema.json'), 'utf8'), +) as Record; + +// `strict: false`: the schema uses `format: "uri"`, which bare ajv does not +// register. This suite tests structure (shapes + typos), not URI formatting. +const ajv = new Ajv({ strict: false, allErrors: true }); +const validate = ajv.compile(schema); + +const SCHEMA_URL = + 'https://raw.githubusercontent.com/graphql-hive/console/main/packages/libraries/cli/hive-config.schema.json'; + +const registry = { + endpoint: 'https://app.graphql-hive.com/graphql', + accessToken: 'a-registry-token', +}; +const cdn = { + endpoint: 'https://cdn.graphql-hive.com/artifacts/v1/target', + accessToken: 'a-cdn-token', +}; + +const validConfigs: Array<[string, unknown]> = [ + ['a modern config with registry only', { registry }], + ['a modern config with registry and cdn', { registry, cdn }], + ['an empty config', {}], + ['a modern config with a $schema reference', { $schema: SCHEMA_URL, registry }], + ['a legacy flat config', { registry: 'https://app.graphql-hive.com/graphql', token: 'a-token' }], + [ + 'a legacy flat config with a $schema reference', + { $schema: SCHEMA_URL, registry: 'https://app.graphql-hive.com/graphql', token: 'a-token' }, + ], + [ + 'a namespaced config with default and named spaces', + { default: { registry }, production: { registry } }, + ], + ['a namespaced config with a $schema reference', { $schema: SCHEMA_URL, default: { registry } }], +]; + +const invalidConfigs: Array<[string, unknown]> = [ + [ + 'a misspelled top-level "registery" key', + { registery: { endpoint: 'https://app.graphql-hive.com/graphql' } }, + ], + [ + 'a misspelled nested "accesToken" key', + { registry: { endpoint: 'https://app.graphql-hive.com/graphql', accesToken: 'a-token' } }, + ], + ['a non-string registry endpoint', { registry: { endpoint: 123 } }], + ['a registry that is neither an object nor a string', { registry: true }], +]; + +describe('hive-config.schema.json', () => { + test('compiles as a valid JSON Schema', () => { + expect(typeof validate).toBe('function'); + }); + + test.each(validConfigs)('accepts %s', (_name, config) => { + const isValid = validate(config); + expect(isValid, ajv.errorsText(validate.errors)).toBe(true); + }); + + test.each(invalidConfigs)('rejects %s', (_name, config) => { + const isValid = validate(config); + expect(isValid).toBe(false); + }); +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 742cbecfbd2..16f2e8281c2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -545,6 +545,9 @@ importers: '@types/env-ci': specifier: 3.1.4 version: 3.1.4 + ajv: + specifier: ^8.18.0 + version: 8.18.0 oclif: specifier: 4.22.65 version: 4.22.65(patch_hash=14bfe62909a48d0f0115f257cc517dbaa3caac858fc059ddeaa614af424b626b)(@types/node@25.5.0)