Skip to content

Commit 56a75f3

Browse files
Add typed JSON output to theme publish
1 parent 184ceb6 commit 56a75f3

10 files changed

Lines changed: 393 additions & 30 deletions

File tree

‎docs-shopify.dev/generated/generated_docs_data_v2.json‎

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8042,6 +8042,15 @@
80428042
"isOptional": true,
80438043
"environmentValue": "SHOPIFY_FLAG_FORCE"
80448044
},
8045+
{
8046+
"filePath": "docs-shopify.dev/commands/interfaces/theme-publish.interface.ts",
8047+
"syntaxKind": "PropertySignature",
8048+
"name": "-j, --json",
8049+
"value": "''",
8050+
"description": "Output the result as JSON. Automatically disables color output.",
8051+
"isOptional": true,
8052+
"environmentValue": "SHOPIFY_FLAG_JSON"
8053+
},
80458054
{
80468055
"filePath": "docs-shopify.dev/commands/interfaces/theme-publish.interface.ts",
80478056
"syntaxKind": "PropertySignature",
@@ -8061,7 +8070,7 @@
80618070
"environmentValue": "SHOPIFY_FLAG_THEME_ID"
80628071
}
80638072
],
8064-
"value": "export interface themepublish {\n /**\n * Alias of the Shopify account to use for authentication.\n * @environment SHOPIFY_FLAG_AUTH_ALIAS\n */\n '--auth-alias <value>'?: string\n\n /**\n * The environment to apply to the current command.\n * @environment SHOPIFY_FLAG_ENVIRONMENT\n */\n '-e, --environment <value>'?: string\n\n /**\n * Skip confirmation. Required if non interactive.\n * @environment SHOPIFY_FLAG_FORCE\n */\n '-f, --force'?: ''\n\n /**\n * Print the command's JSON schemas.\n * @environment SHOPIFY_FLAG_JSON_SCHEMA\n */\n '--json-schema'?: ''\n\n /**\n * Disable color output.\n * @environment SHOPIFY_FLAG_NO_COLOR\n */\n '--no-color'?: ''\n\n /**\n * Password generated from the Theme Access app or an Admin API token.\n * @environment SHOPIFY_CLI_THEME_TOKEN\n */\n '--password <value>'?: string\n\n /**\n * The path where you want to run the command. Defaults to the current working directory.\n * @environment SHOPIFY_FLAG_PATH\n */\n '--path <value>'?: string\n\n /**\n * Store URL. It can be the store prefix (example) or the full myshopify.com URL (example.myshopify.com, https://example.myshopify.com).\n * @environment SHOPIFY_FLAG_STORE\n */\n '-s, --store <value>'?: string\n\n /**\n * Theme ID or name of the remote theme. Required if non interactive.\n * @environment SHOPIFY_FLAG_THEME_ID\n */\n '-t, --theme <value>'?: string\n\n /**\n * Increase the verbosity of the output. May include sensitive data.\n * @environment SHOPIFY_FLAG_VERBOSE\n */\n '--verbose'?: ''\n}"
8073+
"value": "export interface themepublish {\n /**\n * Alias of the Shopify account to use for authentication.\n * @environment SHOPIFY_FLAG_AUTH_ALIAS\n */\n '--auth-alias <value>'?: string\n\n /**\n * The environment to apply to the current command.\n * @environment SHOPIFY_FLAG_ENVIRONMENT\n */\n '-e, --environment <value>'?: string\n\n /**\n * Skip confirmation. Required if non interactive.\n * @environment SHOPIFY_FLAG_FORCE\n */\n '-f, --force'?: ''\n\n /**\n * Output the result as JSON. Automatically disables color output.\n * @environment SHOPIFY_FLAG_JSON\n */\n '-j, --json'?: ''\n\n /**\n * Print the command's JSON schemas.\n * @environment SHOPIFY_FLAG_JSON_SCHEMA\n */\n '--json-schema'?: ''\n\n /**\n * Disable color output.\n * @environment SHOPIFY_FLAG_NO_COLOR\n */\n '--no-color'?: ''\n\n /**\n * Password generated from the Theme Access app or an Admin API token.\n * @environment SHOPIFY_CLI_THEME_TOKEN\n */\n '--password <value>'?: string\n\n /**\n * The path where you want to run the command. Defaults to the current working directory.\n * @environment SHOPIFY_FLAG_PATH\n */\n '--path <value>'?: string\n\n /**\n * Store URL. It can be the store prefix (example) or the full myshopify.com URL (example.myshopify.com, https://example.myshopify.com).\n * @environment SHOPIFY_FLAG_STORE\n */\n '-s, --store <value>'?: string\n\n /**\n * Theme ID or name of the remote theme. Required if non interactive.\n * @environment SHOPIFY_FLAG_THEME_ID\n */\n '-t, --theme <value>'?: string\n\n /**\n * Increase the verbosity of the output. May include sensitive data.\n * @environment SHOPIFY_FLAG_VERBOSE\n */\n '--verbose'?: ''\n}"
80658074
}
80668075
},
80678076
"themepull": {

‎packages/cli/README.md‎

Lines changed: 101 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8479,7 +8479,7 @@ Set a remote theme as the live theme.
84798479
84808480
```
84818481
USAGE
8482-
$ shopify theme publish [--auth-alias <value>] [-e <value>...] [-f] [--json-schema] [--no-color] [--password
8482+
$ shopify theme publish [--auth-alias <value>] [-e <value>...] [-f] [-j] [--json-schema] [--no-color] [--password
84838483
<value>] [--path <value>] [-s <value>] [-t <value>] [--verbose]
84848484

84858485
FLAGS
@@ -8491,6 +8491,10 @@ FLAGS
84918491
Skip confirmation. Required if non interactive.
84928492
[env: SHOPIFY_FLAG_FORCE]
84938493

8494+
-j, --json
8495+
Output the result as JSON. Automatically disables color output.
8496+
[env: SHOPIFY_FLAG_JSON]
8497+
84948498
-s, --store=<value>
84958499
Store URL. It can be the store prefix (example) or the full myshopify.com URL (example.myshopify.com,
84968500
https://example.myshopify.com).
@@ -8537,6 +8541,102 @@ DESCRIPTION
85378541

85388542
If you want to publish your local theme, then you need to run `shopify theme push` first. You're asked to confirm that
85398543
you want to publish the specified theme. You can skip this confirmation using the `--force` flag.
8544+
8545+
With `--json`, returns the published theme. Multiple environments return one document in configured order, omitting
8546+
failed environments and reporting their errors on stderr. Exit behavior is unchanged.
8547+
8548+
Output from `--json` conforms to the `ThemePublishResult` schema.
8549+
8550+
Use `--json-schema` to print the result, error, and event schemas.
8551+
8552+
```json
8553+
{
8554+
"anyOf": [
8555+
{
8556+
"$ref": "#/definitions/ThemePublishEnvironment/properties/result"
8557+
},
8558+
{
8559+
"type": "object",
8560+
"properties": {
8561+
"environments": {
8562+
"type": "array",
8563+
"items": {
8564+
"$ref": "#/definitions/ThemePublishEnvironment"
8565+
}
8566+
}
8567+
},
8568+
"required": [
8569+
"environments"
8570+
],
8571+
"additionalProperties": false
8572+
}
8573+
],
8574+
"title": "ThemePublishResult",
8575+
"definitions": {
8576+
"PublishedTheme": {
8577+
"type": "object",
8578+
"properties": {
8579+
"id": {
8580+
"type": "number"
8581+
},
8582+
"name": {
8583+
"type": "string"
8584+
},
8585+
"role": {
8586+
"type": "string"
8587+
},
8588+
"processing": {
8589+
"type": "boolean"
8590+
},
8591+
"createdAtRuntime": {
8592+
"type": "boolean"
8593+
},
8594+
"src": {
8595+
"type": "string"
8596+
},
8597+
"shop": {
8598+
"type": "string"
8599+
}
8600+
},
8601+
"required": [
8602+
"id",
8603+
"name",
8604+
"role",
8605+
"processing",
8606+
"createdAtRuntime",
8607+
"shop"
8608+
],
8609+
"additionalProperties": false
8610+
},
8611+
"ThemePublishEnvironment": {
8612+
"type": "object",
8613+
"properties": {
8614+
"environment": {
8615+
"type": "string"
8616+
},
8617+
"result": {
8618+
"type": "object",
8619+
"properties": {
8620+
"theme": {
8621+
"$ref": "#/definitions/PublishedTheme"
8622+
}
8623+
},
8624+
"required": [
8625+
"theme"
8626+
],
8627+
"additionalProperties": false
8628+
}
8629+
},
8630+
"required": [
8631+
"environment",
8632+
"result"
8633+
],
8634+
"additionalProperties": false
8635+
}
8636+
},
8637+
"$schema": "http://json-schema.org/draft-07/schema#"
8638+
}
8639+
```
85408640
```
85418641
85428642
## `shopify theme pull`

‎packages/cli/oclif.manifest.json‎

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11052,8 +11052,8 @@
1105211052
"args": {
1105311053
},
1105411054
"customPluginName": "@shopify/theme",
11055-
"description": "Publishes an unpublished theme from your theme library.\n\nIf no theme ID is specified, then you're prompted to select the theme that you want to publish from the list of themes in your store.\n\nYou 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\nIf you want to publish your local theme, then you need to run `shopify theme push` first. You're asked to confirm that you want to publish the specified theme. You can skip this confirmation using the `--force` flag.",
11056-
"descriptionWithMarkdown": "Publishes an unpublished theme from your theme library.\n\nIf no theme ID is specified, then you're prompted to select the theme that you want to publish from the list of themes in your store.\n\nYou 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\nIf you want to publish your local theme, then you need to run `shopify theme push` first. You're asked to confirm that you want to publish the specified theme. You can skip this confirmation using the `--force` flag.",
11055+
"description": "Publishes an unpublished theme from your theme library.\n\nIf no theme ID is specified, then you're prompted to select the theme that you want to publish from the list of themes in your store.\n\nYou 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\nIf you want to publish your local theme, then you need to run `shopify theme push` first. You're asked to confirm that you want to publish the specified theme. You can skip this confirmation using the `--force` flag.\n\nWith `--json`, returns the published theme. Multiple environments return one document in configured order, omitting failed environments and reporting their errors on stderr. Exit behavior is unchanged.\n\nOutput from `--json` conforms to the `ThemePublishResult` schema.\n\nUse `--json-schema` to print the result, error, and event schemas.\n\n```json\n{\n \"anyOf\": [\n {\n \"$ref\": \"#/definitions/ThemePublishEnvironment/properties/result\"\n },\n {\n \"type\": \"object\",\n \"properties\": {\n \"environments\": {\n \"type\": \"array\",\n \"items\": {\n \"$ref\": \"#/definitions/ThemePublishEnvironment\"\n }\n }\n },\n \"required\": [\n \"environments\"\n ],\n \"additionalProperties\": false\n }\n ],\n \"title\": \"ThemePublishResult\",\n \"definitions\": {\n \"PublishedTheme\": {\n \"type\": \"object\",\n \"properties\": {\n \"id\": {\n \"type\": \"number\"\n },\n \"name\": {\n \"type\": \"string\"\n },\n \"role\": {\n \"type\": \"string\"\n },\n \"processing\": {\n \"type\": \"boolean\"\n },\n \"createdAtRuntime\": {\n \"type\": \"boolean\"\n },\n \"src\": {\n \"type\": \"string\"\n },\n \"shop\": {\n \"type\": \"string\"\n }\n },\n \"required\": [\n \"id\",\n \"name\",\n \"role\",\n \"processing\",\n \"createdAtRuntime\",\n \"shop\"\n ],\n \"additionalProperties\": false\n },\n \"ThemePublishEnvironment\": {\n \"type\": \"object\",\n \"properties\": {\n \"environment\": {\n \"type\": \"string\"\n },\n \"result\": {\n \"type\": \"object\",\n \"properties\": {\n \"theme\": {\n \"$ref\": \"#/definitions/PublishedTheme\"\n }\n },\n \"required\": [\n \"theme\"\n ],\n \"additionalProperties\": false\n }\n },\n \"required\": [\n \"environment\",\n \"result\"\n ],\n \"additionalProperties\": false\n }\n },\n \"$schema\": \"http://json-schema.org/draft-07/schema#\"\n}\n```",
11056+
"descriptionWithMarkdown": "Publishes an unpublished theme from your theme library.\n\nIf no theme ID is specified, then you're prompted to select the theme that you want to publish from the list of themes in your store.\n\nYou 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\nIf you want to publish your local theme, then you need to run `shopify theme push` first. You're asked to confirm that you want to publish the specified theme. You can skip this confirmation using the `--force` flag.\n\nWith `--json`, returns the published theme. Multiple environments return one document in configured order, omitting failed environments and reporting their errors on stderr. Exit behavior is unchanged.",
1105711057
"enableJsonFlag": false,
1105811058
"flags": {
1105911059
"auth-alias": {
@@ -11081,6 +11081,15 @@
1108111081
"name": "force",
1108211082
"type": "boolean"
1108311083
},
11084+
"json": {
11085+
"allowNo": false,
11086+
"char": "j",
11087+
"description": "Output the result as JSON. Automatically disables color output.",
11088+
"env": "SHOPIFY_FLAG_JSON",
11089+
"hidden": false,
11090+
"name": "json",
11091+
"type": "boolean"
11092+
},
1108411093
"json-schema": {
1108511094
"allowNo": false,
1108611095
"description": "Print the command's JSON schemas.",

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

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,6 @@ const commandExceptions = [
6666
'packages/theme/src/cli/commands/theme/package.ts',
6767
'packages/theme/src/cli/commands/theme/preview.ts',
6868
'packages/theme/src/cli/commands/theme/profile.ts',
69-
'packages/theme/src/cli/commands/theme/publish.ts',
7069
'packages/theme/src/cli/commands/theme/pull.ts',
7170
'packages/theme/src/cli/commands/theme/push.ts',
7271
'packages/theme/src/cli/commands/theme/rename.ts',
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
import Publish from './publish.js'
2+
import {themePublishJsonOutputSchema} from '../../services/publish/types.js'
3+
import {findOrSelectTheme} from '../../utilities/theme-selector.js'
4+
import {Config} from '@oclif/core'
5+
import {ensureAuthenticatedThemes} from '@shopify/cli-kit/node/session'
6+
import {themePublish} from '@shopify/cli-kit/node/themes/api'
7+
import {withCapturedStandardStreams} from '@shopify/cli-kit/node/testing/output'
8+
import {runWithCommandEventsForCommand} from '@shopify/cli-kit/node/command-events'
9+
import {inTemporaryDirectory, writeFile} from '@shopify/cli-kit/node/fs'
10+
import {joinPath} from '@shopify/cli-kit/node/path'
11+
import {outputWarn} from '@shopify/cli-kit/node/output'
12+
import {describe, expect, test, vi} from 'vitest'
13+
14+
vi.mock('@shopify/cli-kit/node/session')
15+
vi.mock('@shopify/cli-kit/node/themes/api')
16+
vi.mock('@shopify/cli-kit/node/metadata')
17+
vi.mock('../../utilities/theme-selector.js')
18+
vi.mock('../../utilities/theme-store.js', () => ({ensureThemeStore: ({store}: {store: string}) => store}))
19+
20+
const originalTheme = {id: 1, name: 'Original', role: 'unpublished', processing: false, createdAtRuntime: false}
21+
const publishedTheme = {...originalTheme, role: 'live'}
22+
const store = 'test.myshopify.com'
23+
24+
async function run(argv: string[]) {
25+
const config = new Config({root: __dirname})
26+
await config.load()
27+
vi.mocked(ensureAuthenticatedThemes).mockImplementation(async (storeFqdn) => ({token: 'token', storeFqdn}))
28+
return runWithCommandEventsForCommand(argv, () => new Publish(argv, config).run())
29+
}
30+
31+
async function inEnvironments(run: () => Promise<void>) {
32+
await inTemporaryDirectory(async (directory) => {
33+
await writeFile(
34+
joinPath(directory, 'shopify.theme.toml'),
35+
`
36+
[environments.first]
37+
store = "first.myshopify.com"
38+
password = "token"
39+
[environments.second]
40+
store = "second.myshopify.com"
41+
password = "token"
42+
`,
43+
)
44+
const cwd = vi.spyOn(process, 'cwd').mockReturnValue(directory)
45+
try {
46+
await run()
47+
} finally {
48+
cwd.mockRestore()
49+
}
50+
})
51+
}
52+
53+
describe('theme publish JSON output', () => {
54+
test('exposes its schema and JSON flag in help', () => {
55+
expect(Publish.jsonOutputSchema).toBe(themePublishJsonOutputSchema)
56+
expect(Publish.flags.json).toBeDefined()
57+
expect(Publish.description).toContain('ThemePublishResult')
58+
})
59+
60+
test.each([undefined, '', 'https://example.com/theme.zip'])(
61+
'returns the updated theme and omits missing src (%s)',
62+
async (src) => {
63+
vi.mocked(findOrSelectTheme).mockResolvedValue(originalTheme)
64+
vi.mocked(themePublish).mockResolvedValue({...publishedTheme, src})
65+
await withCapturedStandardStreams(async ({stdout, stderr}) => {
66+
await run(['--store', store, '--theme', '1', '--force', '--json'])
67+
expect(JSON.parse(stdout())).toEqual({
68+
theme: {...publishedTheme, ...(src === undefined ? {} : {src}), shop: store},
69+
})
70+
expect(stderr()).toBe('')
71+
})
72+
},
73+
)
74+
75+
test('routes diagnostics to stderr', async () => {
76+
vi.mocked(findOrSelectTheme).mockResolvedValue(originalTheme)
77+
vi.mocked(themePublish).mockImplementation(async () => {
78+
outputWarn('Retrying request')
79+
return publishedTheme
80+
})
81+
await withCapturedStandardStreams(async ({stdout, stderr}) => {
82+
await run(['--store', store, '--theme', '1', '--force', '--json'])
83+
expect(JSON.parse(stdout()).theme.role).toBe('live')
84+
expect(JSON.parse(stderr())).toMatchObject({type: 'diagnostic', level: 'warning', message: 'Retrying request'})
85+
})
86+
})
87+
88+
test('leaves execution failures to the shared error handler without writing a result', async () => {
89+
vi.mocked(findOrSelectTheme).mockResolvedValue(originalTheme)
90+
vi.mocked(themePublish).mockRejectedValue(new Error('Publishing failed'))
91+
await withCapturedStandardStreams(async ({stdout}) => {
92+
await expect(run(['--store', store, '--theme', '1', '--force', '--json'])).rejects.toThrow('Publishing failed')
93+
expect(stdout()).toBe('')
94+
})
95+
})
96+
97+
test.each(['none', 'partial', 'all'])('collects one document in configured order (%s failures)', async (failures) => {
98+
vi.mocked(findOrSelectTheme).mockResolvedValue(originalTheme)
99+
vi.mocked(themePublish).mockImplementation(async (_id, session) => {
100+
if (failures === 'all' || (failures === 'partial' && session.storeFqdn.startsWith('first.'))) {
101+
throw new Error('Publishing failed')
102+
}
103+
return publishedTheme
104+
})
105+
const exitCode = process.exitCode
106+
await inEnvironments(async () => {
107+
await withCapturedStandardStreams(async ({stdout, stderr}) => {
108+
await run(['--environment', 'first', '--environment', 'second', '--theme', '1', '--force', '--json'])
109+
const environments: string[] = []
110+
if (failures === 'none') environments.push('first')
111+
if (failures !== 'all') environments.push('second')
112+
expect(JSON.parse(stdout())).toEqual({
113+
environments: environments.map((environment) => ({
114+
environment,
115+
result: {theme: {...publishedTheme, shop: `${environment}.myshopify.com`}},
116+
})),
117+
})
118+
if (failures === 'none') {
119+
expect(stderr()).toBe('')
120+
} else {
121+
const errors = stderr()
122+
.trim()
123+
.split('\n')
124+
.map((line) => JSON.parse(line))
125+
expect(errors).toHaveLength(failures === 'all' ? 2 : 1)
126+
expect(errors[0]).toMatchObject({type: 'diagnostic', level: 'error', code: 'theme-environment-failed'})
127+
}
128+
expect(process.exitCode).toBe(exitCode)
129+
})
130+
})
131+
})
132+
133+
test.each([
134+
{theme: {...publishedTheme, id: '1', shop: store}},
135+
{theme: {...publishedTheme, shop: null}},
136+
{theme: {...publishedTheme, src: false, shop: store}},
137+
{environments: [{environment: 'first', result: {theme: {...publishedTheme, role: null, shop: store}}}]},
138+
])('rejects malformed results %#', (result) => {
139+
expect(() => themePublishJsonOutputSchema.validate(result)).toThrow()
140+
})
141+
})

0 commit comments

Comments
 (0)