Skip to content

Commit 96095dc

Browse files
Add typed JSON output to theme push
1 parent 7336e0a commit 96095dc

17 files changed

Lines changed: 846 additions & 200 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@shopify/cli": patch
3+
---
4+
5+
Add a discoverable theme push JSON schema and emit one result document with diagnostics on stderr.

‎packages/cli/README.md‎

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8840,6 +8840,10 @@ DESCRIPTION
88408840
If you use the `--json` flag, then theme information is returned in JSON format, which can be used as a
88418841
machine-readable input for scripts or continuous integration.
88428842

8843+
With multiple environments, JSON output is one array of successful results in the requested environment order. Each
8844+
result includes its environment name. Failed or skipped environments are reported on stderr and omitted; if none
8845+
succeed, the result is an empty array. Existing exit behavior is unchanged.
8846+
88438847
Sample output:
88448848

88458849
```json
@@ -8854,6 +8858,100 @@ DESCRIPTION
88548858
}
88558859
}
88568860
```
8861+
8862+
8863+
Output from `--json` conforms to the `ThemePushJsonResult` schema.
8864+
8865+
Use `--json-schema` to print the result, error, and event schemas.
8866+
8867+
```json
8868+
{
8869+
"anyOf": [
8870+
{
8871+
"type": "object",
8872+
"properties": {
8873+
"environment": {
8874+
"type": "string"
8875+
},
8876+
"theme": {
8877+
"$ref": "#/definitions/ThemePushTheme"
8878+
}
8879+
},
8880+
"required": [
8881+
"theme"
8882+
],
8883+
"additionalProperties": false
8884+
},
8885+
{
8886+
"type": "array",
8887+
"items": {
8888+
"type": "object",
8889+
"properties": {
8890+
"environment": {
8891+
"type": "string"
8892+
},
8893+
"theme": {
8894+
"$ref": "#/definitions/ThemePushTheme"
8895+
}
8896+
},
8897+
"required": [
8898+
"environment",
8899+
"theme"
8900+
],
8901+
"additionalProperties": false
8902+
}
8903+
}
8904+
],
8905+
"title": "ThemePushJsonResult",
8906+
"definitions": {
8907+
"ThemePushTheme": {
8908+
"type": "object",
8909+
"properties": {
8910+
"id": {
8911+
"type": "number"
8912+
},
8913+
"name": {
8914+
"type": "string"
8915+
},
8916+
"role": {
8917+
"type": "string"
8918+
},
8919+
"shop": {
8920+
"type": "string"
8921+
},
8922+
"editor_url": {
8923+
"type": "string"
8924+
},
8925+
"preview_url": {
8926+
"type": "string"
8927+
},
8928+
"warning": {
8929+
"type": "string"
8930+
},
8931+
"errors": {
8932+
"type": "object",
8933+
"additionalProperties": {
8934+
"type": "array",
8935+
"items": {
8936+
"type": "string"
8937+
}
8938+
}
8939+
}
8940+
},
8941+
"required": [
8942+
"id",
8943+
"name",
8944+
"role",
8945+
"shop",
8946+
"editor_url",
8947+
"preview_url"
8948+
],
8949+
"additionalProperties": false
8950+
}
8951+
},
8952+
"$schema": "http://json-schema.org/draft-07/schema#"
8953+
}
8954+
```
88578955
```
88588956
88598957
## `shopify theme rename`

‎packages/cli/oclif.manifest.json‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11327,8 +11327,8 @@
1132711327
"args": {
1132811328
},
1132911329
"customPluginName": "@shopify/theme",
11330-
"description": "Uploads your local theme files to Shopify, overwriting the remote version if specified.\n\n If no theme is specified, then you're prompted to select the theme to overwrite from the list of the themes in your store.\n\n You can run this command only in a directory that matches the \"default Shopify theme folder structure\" (https://shopify.dev/docs/themes/tools/cli#directory-structure).\n\n This command returns the following information:\n\n - A link to the \"editor\" (https://shopify.dev/docs/themes/tools/online-editor) for the theme in the Shopify admin.\n - A \"preview link\" (https://help.shopify.com/manual/online-store/themes/adding-themes#share-a-theme-preview-with-others) that you can share with others.\n\n If you use the `--json` flag, then theme information is returned in JSON format, which can be used as a machine-readable input for scripts or continuous integration.\n\n Sample output:\n\n ```json\n {\n \"theme\": {\n \"id\": 108267175958,\n \"name\": \"MyTheme\",\n \"role\": \"unpublished\",\n \"shop\": \"mystore.myshopify.com\",\n \"editor_url\": \"https://mystore.myshopify.com/admin/themes/108267175958/editor\",\n \"preview_url\": \"https://mystore.myshopify.com/?preview_theme_id=108267175958\"\n }\n }\n ```\n ",
11331-
"descriptionWithMarkdown": "Uploads your local theme files to Shopify, overwriting the remote version if specified.\n\n If no theme is specified, then you're prompted to select the theme to overwrite from the list of the themes in your store.\n\n You can run this command only in a directory that matches the [default Shopify theme folder structure](https://shopify.dev/docs/themes/tools/cli#directory-structure).\n\n This command returns the following information:\n\n - A link to the [editor](https://shopify.dev/docs/themes/tools/online-editor) for the theme in the Shopify admin.\n - A [preview link](https://help.shopify.com/manual/online-store/themes/adding-themes#share-a-theme-preview-with-others) that you can share with others.\n\n If you use the `--json` flag, then theme information is returned in JSON format, which can be used as a machine-readable input for scripts or continuous integration.\n\n Sample output:\n\n ```json\n {\n \"theme\": {\n \"id\": 108267175958,\n \"name\": \"MyTheme\",\n \"role\": \"unpublished\",\n \"shop\": \"mystore.myshopify.com\",\n \"editor_url\": \"https://mystore.myshopify.com/admin/themes/108267175958/editor\",\n \"preview_url\": \"https://mystore.myshopify.com/?preview_theme_id=108267175958\"\n }\n }\n ```\n ",
11330+
"description": "Uploads your local theme files to Shopify, overwriting the remote version if specified.\n\n If no theme is specified, then you're prompted to select the theme to overwrite from the list of the themes in your store.\n\n You can run this command only in a directory that matches the \"default Shopify theme folder structure\" (https://shopify.dev/docs/themes/tools/cli#directory-structure).\n\n This command returns the following information:\n\n - A link to the \"editor\" (https://shopify.dev/docs/themes/tools/online-editor) for the theme in the Shopify admin.\n - A \"preview link\" (https://help.shopify.com/manual/online-store/themes/adding-themes#share-a-theme-preview-with-others) that you can share with others.\n\n If you use the `--json` flag, then theme information is returned in JSON format, which can be used as a machine-readable input for scripts or continuous integration.\n\n With multiple environments, JSON output is one array of successful results in the requested environment order. Each result includes its environment name. Failed or skipped environments are reported on stderr and omitted; if none succeed, the result is an empty array. Existing exit behavior is unchanged.\n\n Sample output:\n\n ```json\n {\n \"theme\": {\n \"id\": 108267175958,\n \"name\": \"MyTheme\",\n \"role\": \"unpublished\",\n \"shop\": \"mystore.myshopify.com\",\n \"editor_url\": \"https://mystore.myshopify.com/admin/themes/108267175958/editor\",\n \"preview_url\": \"https://mystore.myshopify.com/?preview_theme_id=108267175958\"\n }\n }\n ```\n \n\nOutput from `--json` conforms to the `ThemePushJsonResult` schema.\n\nUse `--json-schema` to print the result, error, and event schemas.\n\n```json\n{\n \"anyOf\": [\n {\n \"type\": \"object\",\n \"properties\": {\n \"environment\": {\n \"type\": \"string\"\n },\n \"theme\": {\n \"$ref\": \"#/definitions/ThemePushTheme\"\n }\n },\n \"required\": [\n \"theme\"\n ],\n \"additionalProperties\": false\n },\n {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"properties\": {\n \"environment\": {\n \"type\": \"string\"\n },\n \"theme\": {\n \"$ref\": \"#/definitions/ThemePushTheme\"\n }\n },\n \"required\": [\n \"environment\",\n \"theme\"\n ],\n \"additionalProperties\": false\n }\n }\n ],\n \"title\": \"ThemePushJsonResult\",\n \"definitions\": {\n \"ThemePushTheme\": {\n \"type\": \"object\",\n \"properties\": {\n \"id\": {\n \"type\": \"number\"\n },\n \"name\": {\n \"type\": \"string\"\n },\n \"role\": {\n \"type\": \"string\"\n },\n \"shop\": {\n \"type\": \"string\"\n },\n \"editor_url\": {\n \"type\": \"string\"\n },\n \"preview_url\": {\n \"type\": \"string\"\n },\n \"warning\": {\n \"type\": \"string\"\n },\n \"errors\": {\n \"type\": \"object\",\n \"additionalProperties\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\"\n }\n }\n }\n },\n \"required\": [\n \"id\",\n \"name\",\n \"role\",\n \"shop\",\n \"editor_url\",\n \"preview_url\"\n ],\n \"additionalProperties\": false\n }\n },\n \"$schema\": \"http://json-schema.org/draft-07/schema#\"\n}\n```",
11331+
"descriptionWithMarkdown": "Uploads your local theme files to Shopify, overwriting the remote version if specified.\n\n If no theme is specified, then you're prompted to select the theme to overwrite from the list of the themes in your store.\n\n You can run this command only in a directory that matches the [default Shopify theme folder structure](https://shopify.dev/docs/themes/tools/cli#directory-structure).\n\n This command returns the following information:\n\n - A link to the [editor](https://shopify.dev/docs/themes/tools/online-editor) for the theme in the Shopify admin.\n - A [preview link](https://help.shopify.com/manual/online-store/themes/adding-themes#share-a-theme-preview-with-others) that you can share with others.\n\n If you use the `--json` flag, then theme information is returned in JSON format, which can be used as a machine-readable input for scripts or continuous integration.\n\n With multiple environments, JSON output is one array of successful results in the requested environment order. Each result includes its environment name. Failed or skipped environments are reported on stderr and omitted; if none succeed, the result is an empty array. Existing exit behavior is unchanged.\n\n Sample output:\n\n ```json\n {\n \"theme\": {\n \"id\": 108267175958,\n \"name\": \"MyTheme\",\n \"role\": \"unpublished\",\n \"shop\": \"mystore.myshopify.com\",\n \"editor_url\": \"https://mystore.myshopify.com/admin/themes/108267175958/editor\",\n \"preview_url\": \"https://mystore.myshopify.com/?preview_theme_id=108267175958\"\n }\n }\n ```\n ",
1133211332
"enableJsonFlag": false,
1133311333
"flags": {
1133411334
"allow-live": {

‎packages/eslint-plugin-cli/rules/json-output-command-exceptions.js‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,6 @@ const commandExceptions = [
6767
'packages/theme/src/cli/commands/theme/preview.ts',
6868
'packages/theme/src/cli/commands/theme/profile.ts',
6969
'packages/theme/src/cli/commands/theme/pull.ts',
70-
'packages/theme/src/cli/commands/theme/push.ts',
7170
'packages/theme/src/cli/commands/theme/share.ts',
7271

7372
// Streaming commands without a single finite result.
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import Push from './push.js'
2+
import {executeThemePush} from '../../services/push.js'
3+
import {checkThemeBeforePush} from '../../services/push/result.js'
4+
import {themePushJsonOutputSchema} from '../../services/push/types.js'
5+
import {captureStandardStreams} from '../../utilities/test-helpers/stdio.js'
6+
import {describe, expect, test, vi} from 'vitest'
7+
import {Config} from '@oclif/core'
8+
import {AbortError} from '@shopify/cli-kit/node/error'
9+
10+
vi.mock('../../services/push.js')
11+
vi.mock('../../services/push/result.js', async (importOriginal) => ({
12+
...(await importOriginal<typeof import('../../services/push/result.js')>()),
13+
checkThemeBeforePush: vi.fn(),
14+
}))
15+
16+
const session = {storeFqdn: 'test.myshopify.com', token: 'token'}
17+
18+
describe('theme push JSON', () => {
19+
test('exposes the schema in help and retains JSON and inherited flags', () => {
20+
expect(Push.jsonOutputSchema).toBe(themePushJsonOutputSchema)
21+
expect(Push.description).toContain('ThemePushJsonResult')
22+
expect(Push.flags.json).toBeDefined()
23+
expect(Push.baseFlags).toHaveProperty('json-schema')
24+
})
25+
26+
test('executes and writes the result through the real encoder', async () => {
27+
vi.mocked(executeThemePush).mockResolvedValue({
28+
theme: {
29+
id: 1,
30+
name: 'Theme',
31+
role: 'unpublished',
32+
shop: session.storeFqdn,
33+
editor_url: 'editor',
34+
preview_url: 'preview',
35+
},
36+
published: false,
37+
hasErrors: false,
38+
errors: {},
39+
})
40+
const command = new Push([], new Config({root: '.'}))
41+
const streams = await captureStandardStreams(() => command.command({json: true} as never, session, false))
42+
expect(JSON.parse(streams.stdout)).toEqual({
43+
theme: {
44+
id: 1,
45+
name: 'Theme',
46+
role: 'unpublished',
47+
shop: session.storeFqdn,
48+
editor_url: 'editor',
49+
preview_url: 'preview',
50+
},
51+
})
52+
expect(streams.stderr).toBe('')
53+
})
54+
55+
test('retains cancellation without emitting a success result', async () => {
56+
vi.mocked(executeThemePush).mockResolvedValue(undefined)
57+
const command = new Push([], new Config({root: '.'}))
58+
const streams = await captureStandardStreams(() => command.command({json: true} as never, session, false))
59+
expect(streams.stdout).toBe('')
60+
})
61+
62+
test('propagates strict failures without pushing or writing a result', async () => {
63+
const failure = new AbortError('Theme check failed. Please fix the errors before pushing.')
64+
vi.mocked(checkThemeBeforePush).mockRejectedValueOnce(failure)
65+
const command = new Push([], new Config({root: '.'}))
66+
const streams = await captureStandardStreams(async () => {
67+
await expect(command.command({json: true, strict: true} as never, session, false)).rejects.toBe(failure)
68+
})
69+
expect(executeThemePush).not.toHaveBeenCalled()
70+
expect(streams.stdout).toBe('')
71+
})
72+
})

‎packages/theme/src/cli/commands/theme/push.ts‎

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
11
import {globFlags, themeFlags} from '../../flags.js'
22
import ThemeCommand from '../../utilities/theme-command.js'
3-
import {push} from '../../services/push.js'
3+
import {executeThemePush} from '../../services/push.js'
4+
import {themePushJsonOutputSchema} from '../../services/push/types.js'
5+
import {
6+
checkThemeBeforePush,
7+
renderThemePushResult,
8+
renderThemePushEnvironmentResults,
9+
} from '../../services/push/result.js'
410
import {Flags} from '@oclif/core'
511
import {globalFlags, jsonFlag} from '@shopify/cli-kit/node/cli'
612
import {recordTiming} from '@shopify/cli-kit/node/analytics'
@@ -13,6 +19,10 @@ import type {NonTTYFlagRequirement} from '@shopify/cli-kit/node/base-command'
1319
type PushFlags = InferredFlags<typeof Push.flags>
1420

1521
export default class Push extends ThemeCommand {
22+
static get jsonOutputSchema() {
23+
return themePushJsonOutputSchema
24+
}
25+
1626
static summary = 'Uploads your local theme files to the connected store, overwriting the remote version if specified.'
1727

1828
static usage = ['theme push', 'theme push --unpublished --json']
@@ -30,6 +40,8 @@ export default class Push extends ThemeCommand {
3040
3141
If you use the \`--json\` flag, then theme information is returned in JSON format, which can be used as a machine-readable input for scripts or continuous integration.
3242
43+
With multiple environments, JSON output is one array of successful results in the requested environment order. Each result includes its environment name. Failed or skipped environments are reported on stderr and omitted; if none succeed, the result is an empty array. Existing exit behavior is unchanged.
44+
3345
Sample output:
3446
3547
\`\`\`json
@@ -137,7 +149,8 @@ export default class Push extends ThemeCommand {
137149
context?: {stdout?: Writable; stderr?: Writable},
138150
) {
139151
recordTiming('theme-command:push')
140-
await push(
152+
await checkThemeBeforePush(flags)
153+
const result = await executeThemePush(
141154
{
142155
...flags,
143156
allowLive: flags['allow-live'],
@@ -148,7 +161,17 @@ export default class Push extends ThemeCommand {
148161
multiEnvironment,
149162
context,
150163
)
164+
if (result && !(flags.json && multiEnvironment)) renderThemePushResult(result, flags.json ? 'json' : 'text')
151165
recordTiming('theme-command:push')
166+
return result
167+
}
168+
169+
protected collectsEnvironmentResults(flags: {json?: boolean}): boolean {
170+
return Boolean(flags.json)
171+
}
172+
173+
protected renderEnvironmentResults(results: {environment: string; result: unknown}[]): void {
174+
renderThemePushEnvironmentResults(results)
152175
}
153176

154177
protected storeAuthScopes(): string[] {

0 commit comments

Comments
 (0)