Skip to content

Commit 4f412b3

Browse files
Add typed JSON output to theme push
1 parent 32c0974 commit 4f412b3

16 files changed

Lines changed: 841 additions & 200 deletions

File tree

‎packages/cli/README.md‎

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

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

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