-
Notifications
You must be signed in to change notification settings - Fork 104
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat(@graphql-hive/cli): json output #6122
base: main
Are you sure you want to change the base?
Changes from all commits
7dc21e7
de6c0dc
b5f78b4
31eb025
27e8811
195e466
3328e20
f81c69c
2423a50
d4bd1bf
35da5f6
fbc286e
6a4a46b
1e3c14f
8f68b79
c9628bb
528bad9
e8b65ec
fe15732
0d5d33f
333095c
79f7763
1fbbcc7
cf6074c
f8986a6
754526c
321325d
923c422
032ff42
ba14c2d
ad7e65c
d4f1b88
392ad0f
6b9aba5
af8721b
def2053
d631f8b
219faf5
58b62b8
dee2049
2221fa0
6d498eb
b258db2
97c46dd
9439aa7
612aaa4
7c1823e
3ccd617
a45f5d6
84833da
2b8c7a8
888657a
87fe7c7
4d5644f
7dffe2e
b33e865
5796094
247cf5c
35efce1
b0e966e
45af397
e15c9cf
4861b9e
eb0b1e1
609d286
1fa5079
4b3675f
3010a25
58d3c70
9d0fc87
31cdc7c
76eb192
5489204
8b23d0e
77db99a
59b066c
2e675fd
438941c
ee1dcc7
044835a
ba94eea
cd30c27
90afcf7
d9b114d
728af1f
8cf2893
764fb20
64d60d8
028cfde
4fb7f07
07fa446
5d62044
a503c12
35e299e
fd1a85c
c35833d
07b9f97
5049eb3
b774dcf
b3b63d2
f7e3ef0
5444780
af41791
48973fb
f1f6b9f
7943697
f6b12a4
c7c629e
1cd5734
ada91d0
ade8ad3
ad3024b
1f2f2eb
c8dbfaf
ed962e7
3d6b2e7
ae88819
66d174c
d2bfa72
dae187d
6663144
45362ae
71d1bd0
bda183b
33e5025
aaf34ac
2457c33
c5be266
0b7aff4
aba8c15
dacb9a9
83f9083
92b5142
a0fa648
499c7b7
c42ff11
19daa68
b31ad6f
db2d11a
c99c34f
11fb9f8
75bce4a
ea41083
f1ae863
48c56d1
b19fc62
745b1ef
47fae67
953560b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
--- | ||
'@graphql-hive/cli': minor | ||
--- | ||
|
||
Add JSON output mode. | ||
|
||
All commands now have a `--json` flag that will enable JSON output as opposed to plain text. | ||
|
||
Caveats: | ||
|
||
- The `dev` command is not yet supported. | ||
- Common command output cases have been updated to support JSON output, but coverage is not 100% complete, in particular failure cases. | ||
- There is no official documentation for the schema. For now, just try it out to see what outputs you get, or review to the source code. Its accessible: each command has its own module that contains its declaratively defined output schema (using Typebox). | ||
|
||
|
||
Remember that the Hive CLI is pre-1.0.0 and every minor release could bring breaking changes. Going forward we will mention in the changelog any time the schema has breaking changes. |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -71,8 +71,6 @@ jobs: | |
|
||
- name: run integration tests | ||
timeout-minutes: 10 | ||
env: | ||
HIVE_DEBUG: 1 | ||
Comment on lines
-74
to
-75
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This causes stack traces to be rendered by OClif which breaks our CLI snapshot tests. |
||
run: | | ||
VITEST_MAX_THREADS=${{ steps.cpu-cores.outputs.count }} pnpm --filter integration-tests test:integration --shard=${{ matrix.shardIndex }}/3 | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -21,6 +21,7 @@ | |
"@hive/schema": "workspace:*", | ||
"@hive/server": "workspace:*", | ||
"@hive/storage": "workspace:*", | ||
"@sinclair/typebox": "^0.34.12", | ||
jasonkuhrt marked this conversation as resolved.
Show resolved
Hide resolved
|
||
"@trpc/client": "10.45.2", | ||
"@trpc/server": "10.45.2", | ||
"@types/async-retry": "1.4.8", | ||
|
@@ -36,7 +37,7 @@ | |
"slonik": "30.4.4", | ||
"strip-ansi": "7.1.0", | ||
"tslib": "2.8.1", | ||
"vitest": "2.0.5", | ||
"vitest": "2.1.8", | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thought this might be the version that improves terminal watch mode flickering. Doesn't seem to have worked but no matter, upgrade vitest all the same. |
||
"zod": "3.23.8" | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,38 +1,27 @@ | ||
import { randomUUID } from 'node:crypto'; | ||
import { writeFile } from 'node:fs/promises'; | ||
import { tmpdir } from 'node:os'; | ||
import { join, resolve } from 'node:path'; | ||
import { resolve } from 'node:path'; | ||
import { execaCommand } from '@esm2cjs/execa'; | ||
import { fetchLatestSchema, fetchLatestValidSchema } from './flow'; | ||
import { generateTmpFile } from './fs'; | ||
import { getServiceHost } from './utils'; | ||
|
||
const binPath = resolve(__dirname, '../../packages/libraries/cli/bin/run'); | ||
const cliDir = resolve(__dirname, '../../packages/libraries/cli'); | ||
|
||
async function generateTmpFile(content: string, extension: string) { | ||
const dir = tmpdir(); | ||
const fileName = randomUUID(); | ||
const filepath = join(dir, `${fileName}.${extension}`); | ||
|
||
await writeFile(filepath, content, 'utf-8'); | ||
|
||
return filepath; | ||
} | ||
|
||
async function exec(cmd: string) { | ||
const outout = await execaCommand(`${binPath} ${cmd}`, { | ||
export async function exec(cmd: string) { | ||
const result = await execaCommand(`${binPath} ${cmd}`, { | ||
shell: true, | ||
env: { | ||
OCLIF_CLI_CUSTOM_PATH: cliDir, | ||
NODE_OPTIONS: '--no-deprecation', | ||
}, | ||
}); | ||
|
||
if (outout.failed) { | ||
throw new Error(outout.stderr); | ||
if (result.failed) { | ||
throw new Error('CLI execution marked as "failed".', { cause: result.stderr }); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. While trying to understand where stack traces and errors were coming from, I adjusted this to be more explicit. |
||
} | ||
|
||
return outout.stdout; | ||
return result.stdout; | ||
} | ||
|
||
export async function schemaPublish(args: string[]) { | ||
|
@@ -70,6 +59,8 @@ async function dev(args: string[]) { | |
); | ||
} | ||
|
||
export type CLI = ReturnType<typeof createCLI>; | ||
|
||
export function createCLI(tokens: { readwrite: string; readonly: string }) { | ||
let publishCount = 0; | ||
|
||
|
@@ -81,6 +72,7 @@ export function createCLI(tokens: { readwrite: string; readonly: string }) { | |
expect: expectedStatus, | ||
legacy_force, | ||
legacy_acceptBreakingChanges, | ||
json, | ||
}: { | ||
sdl: string; | ||
commit?: string; | ||
|
@@ -90,6 +82,7 @@ export function createCLI(tokens: { readwrite: string; readonly: string }) { | |
legacy_force?: boolean; | ||
legacy_acceptBreakingChanges?: boolean; | ||
expect: 'latest' | 'latest-composable' | 'ignored' | 'rejected'; | ||
json?: boolean; | ||
}): Promise<string> { | ||
const publishName = ` #${++publishCount}`; | ||
const commit = randomUUID(); | ||
|
@@ -106,6 +99,7 @@ export function createCLI(tokens: { readwrite: string; readonly: string }) { | |
...(metadata ? ['--metadata', await generateTmpFile(JSON.stringify(metadata), 'json')] : []), | ||
...(legacy_force ? ['--force'] : []), | ||
...(legacy_acceptBreakingChanges ? ['--experimental_acceptBreakingChanges'] : []), | ||
...(json ? ['--json'] : []), | ||
await generateTmpFile(sdl, 'graphql'), | ||
]); | ||
|
||
|
@@ -177,15 +171,18 @@ export function createCLI(tokens: { readwrite: string; readonly: string }) { | |
sdl, | ||
serviceName, | ||
expect: expectedStatus, | ||
json, | ||
}: { | ||
sdl: string; | ||
serviceName?: string; | ||
expect: 'approved' | 'rejected'; | ||
json?: boolean; | ||
}): Promise<string> { | ||
const cmd = schemaCheck([ | ||
'--registry.accessToken', | ||
tokens.readonly, | ||
...(serviceName ? ['--service', serviceName] : []), | ||
...(json ? ['--json'] : []), | ||
await generateTmpFile(sdl, 'graphql'), | ||
]); | ||
|
||
|
@@ -199,11 +196,19 @@ export function createCLI(tokens: { readwrite: string; readonly: string }) { | |
async function deleteCommand({ | ||
serviceName, | ||
expect: expectedStatus, | ||
json, | ||
}: { | ||
serviceName?: string; | ||
json?: boolean; | ||
expect: 'latest' | 'latest-composable' | 'rejected'; | ||
}): Promise<string> { | ||
const cmd = schemaDelete(['--token', tokens.readwrite, '--confirm', serviceName ?? '']); | ||
const cmd = schemaDelete([ | ||
'--token', | ||
tokens.readwrite, | ||
'--confirm', | ||
serviceName ?? '', | ||
...(json ? ['--json'] : []), | ||
]); | ||
|
||
const before = { | ||
latest: await fetchLatestSchema(tokens.readonly).then(r => r.expectNoGraphQLErrors()), | ||
|
@@ -259,11 +264,13 @@ export function createCLI(tokens: { readwrite: string; readonly: string }) { | |
url: string; | ||
sdl: string; | ||
}>; | ||
json?: boolean; | ||
remote: boolean; | ||
write?: string; | ||
useLatestVersion?: boolean; | ||
}) { | ||
return dev([ | ||
...(input.json ? ['--json'] : []), | ||
...(input.remote | ||
? [ | ||
'--remote', | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
import { randomUUID } from 'node:crypto'; | ||
import { readFile, writeFile } from 'node:fs/promises'; | ||
import { tmpdir } from 'node:os'; | ||
import { join } from 'node:path'; | ||
|
||
export function tmpFile(extension: string) { | ||
const dir = tmpdir(); | ||
const fileName = randomUUID(); | ||
const filepath = join(dir, `${fileName}.${extension}`); | ||
|
||
return { | ||
filepath, | ||
read() { | ||
return readFile(filepath, 'utf-8'); | ||
}, | ||
}; | ||
} | ||
|
||
export async function generateTmpFile(content: string, extension: string) { | ||
const dir = tmpdir(); | ||
const fileName = randomUUID(); | ||
const filepath = join(dir, `${fileName}.${extension}`); | ||
|
||
await writeFile(filepath, content, 'utf-8'); | ||
|
||
return filepath; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,102 @@ | ||
/** | ||
* This module uses Vitest's fixture system to make common usage patterns | ||
* of our testkit easily consumable in test cases. @see https://vitest.dev/guide/test-context.html#test-extend | ||
*/ | ||
|
||
import { test as testBase } from 'vitest'; | ||
import { CLI, createCLI } from './cli'; | ||
import { ProjectType } from './gql/graphql'; | ||
import { initSeed, OrgSeed, OwnerSeed, ProjectSeed, Seed, TargetAccessTokenSeed } from './seed'; | ||
|
||
interface Context { | ||
seed: Seed; | ||
owner: OwnerSeed; | ||
org: OrgSeed; | ||
// | ||
// "single" branch | ||
// | ||
projectSingle: ProjectSeed; | ||
targetAccessTokenSingle: TargetAccessTokenSeed; | ||
cliSingle: CLI; | ||
// | ||
// "federation" branch | ||
// | ||
projectFederation: ProjectSeed; | ||
targetAccessTokenFederation: TargetAccessTokenSeed; | ||
cliFederation: CLI; | ||
// | ||
// "stitching" branch | ||
// | ||
projectStitching: ProjectSeed; | ||
targetAccessTokenStitching: TargetAccessTokenSeed; | ||
cliStitching: CLI; | ||
} | ||
|
||
export const test = testBase.extend<Context>({ | ||
seed: async ({}, use) => { | ||
const seed = await initSeed(); | ||
await use(seed); | ||
}, | ||
owner: async ({ seed }, use) => { | ||
const owner = await seed.createOwner(); | ||
await use(owner); | ||
}, | ||
org: async ({ owner }, use) => { | ||
const org = await owner.createOrg(); | ||
await use(org); | ||
}, | ||
// | ||
// "single" branch | ||
// | ||
projectSingle: async ({ org }, use) => { | ||
const project = await org.createProject(ProjectType.Single); | ||
await use(project); | ||
}, | ||
targetAccessTokenSingle: async ({ projectSingle }, use) => { | ||
const targetAccessToken = await projectSingle.createTargetAccessToken({}); | ||
await use(targetAccessToken); | ||
}, | ||
cliSingle: async ({ targetAccessTokenSingle }, use) => { | ||
const cli = createCLI({ | ||
readwrite: targetAccessTokenSingle.secret, | ||
readonly: targetAccessTokenSingle.secret, | ||
}); | ||
await use(cli); | ||
}, | ||
// | ||
// "federation" branch | ||
// | ||
projectFederation: async ({ org }, use) => { | ||
const project = await org.createProject(ProjectType.Federation); | ||
await use(project); | ||
}, | ||
targetAccessTokenFederation: async ({ projectFederation }, use) => { | ||
const targetAccessToken = await projectFederation.createTargetAccessToken({}); | ||
await use(targetAccessToken); | ||
}, | ||
cliFederation: async ({ targetAccessTokenFederation }, use) => { | ||
const cli = createCLI({ | ||
readwrite: targetAccessTokenFederation.secret, | ||
readonly: targetAccessTokenFederation.secret, | ||
}); | ||
await use(cli); | ||
}, | ||
// | ||
// "stitching" branch | ||
// | ||
projectStitching: async ({ org }, use) => { | ||
const project = await org.createProject(ProjectType.Stitching); | ||
await use(project); | ||
}, | ||
targetAccessTokenStitching: async ({ projectStitching }, use) => { | ||
const targetAccessToken = await projectStitching.createTargetAccessToken({}); | ||
await use(targetAccessToken); | ||
}, | ||
cliStitching: async ({ targetAccessTokenStitching }, use) => { | ||
const cli = createCLI({ | ||
readwrite: targetAccessTokenStitching.secret, | ||
readonly: targetAccessTokenStitching.secret, | ||
}); | ||
await use(cli); | ||
}, | ||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export * from './cli-output'; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export * as SnapshotSerializers from './_'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Changelog here