From 3c02f70bad8371da27a93188e51e4e133cb98432 Mon Sep 17 00:00:00 2001 From: Gonzalo Riestra Date: Thu, 24 Sep 2026 11:44:13 +0200 Subject: [PATCH 1/5] Add typed JSON output to theme publish --- packages/cli/oclif.manifest.json | 11 +- .../rules/json-output-command-exceptions.js | 1 - .../src/cli/commands/theme/publish.test.ts | 146 ++++++++++++++++++ .../theme/src/cli/commands/theme/publish.ts | 23 ++- .../theme/src/cli/services/publish.test.ts | 48 +++++- packages/theme/src/cli/services/publish.ts | 35 ++--- .../theme/src/cli/services/publish/result.ts | 25 +++ .../theme/src/cli/services/publish/types.ts | 21 +++ 8 files changed, 284 insertions(+), 26 deletions(-) create mode 100644 packages/theme/src/cli/commands/theme/publish.test.ts create mode 100644 packages/theme/src/cli/services/publish/result.ts create mode 100644 packages/theme/src/cli/services/publish/types.ts diff --git a/packages/cli/oclif.manifest.json b/packages/cli/oclif.manifest.json index ac935388674..d714a4e6401 100644 --- a/packages/cli/oclif.manifest.json +++ b/packages/cli/oclif.manifest.json @@ -12331,7 +12331,7 @@ "args": { }, "customPluginName": "@shopify/theme", - "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.", + "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\nUse `--json-schema` to print the result, error, and event schemas.\n\nOutput from `--json` conforms to the `ThemePublishResult` schema.\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 \"status\": {\n \"type\": \"string\",\n \"const\": \"success\"\n },\n \"theme\": {\n \"$ref\": \"#/definitions/PublishedTheme\"\n }\n },\n \"required\": [\n \"status\",\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```", "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.", "enableJsonFlag": false, "flags": { @@ -12360,6 +12360,15 @@ "name": "force", "type": "boolean" }, + "json": { + "allowNo": false, + "char": "j", + "description": "Output the result as JSON. Automatically disables color output.", + "env": "SHOPIFY_FLAG_JSON", + "hidden": false, + "name": "json", + "type": "boolean" + }, "json-schema": { "allowNo": false, "description": "Print the command's JSON schemas.", diff --git a/packages/eslint-plugin-cli/rules/json-output-command-exceptions.js b/packages/eslint-plugin-cli/rules/json-output-command-exceptions.js index f8639f9394c..555743e0282 100644 --- a/packages/eslint-plugin-cli/rules/json-output-command-exceptions.js +++ b/packages/eslint-plugin-cli/rules/json-output-command-exceptions.js @@ -21,7 +21,6 @@ const commandExceptions = [ 'packages/theme/src/cli/commands/theme/package.ts', 'packages/theme/src/cli/commands/theme/preview.ts', 'packages/theme/src/cli/commands/theme/profile.ts', - 'packages/theme/src/cli/commands/theme/publish.ts', 'packages/theme/src/cli/commands/theme/pull.ts', 'packages/theme/src/cli/commands/theme/push.ts', 'packages/theme/src/cli/commands/theme/rename.ts', diff --git a/packages/theme/src/cli/commands/theme/publish.test.ts b/packages/theme/src/cli/commands/theme/publish.test.ts new file mode 100644 index 00000000000..8dcd04ade73 --- /dev/null +++ b/packages/theme/src/cli/commands/theme/publish.test.ts @@ -0,0 +1,146 @@ +import Publish from './publish.js' +import {themePublishJsonOutputSchema} from '../../services/publish/types.js' +import {findOrSelectTheme} from '../../utilities/theme-selector.js' +import {Config} from '@oclif/core' +import {ensureAuthenticatedThemes} from '@shopify/cli-kit/node/session' +import {themePublish} from '@shopify/cli-kit/node/themes/api' +import {withCapturedStandardStreams} from '@shopify/cli-kit/node/testing/output' +import {runWithCommandEventsForCommand} from '@shopify/cli-kit/node/command-events' +import {inTemporaryDirectory, writeFile} from '@shopify/cli-kit/node/fs' +import {joinPath} from '@shopify/cli-kit/node/path' +import {outputWarn} from '@shopify/cli-kit/node/output' +import {describe, expect, test, vi} from 'vitest' + +vi.mock('@shopify/cli-kit/node/session') +vi.mock('@shopify/cli-kit/node/themes/api') +vi.mock('@shopify/cli-kit/node/metadata') +vi.mock('../../utilities/theme-selector.js') +vi.mock('../../utilities/theme-store.js', () => ({ensureThemeStore: ({store}: {store: string}) => store})) + +const originalTheme = {id: 1, name: 'Original', role: 'unpublished', processing: false, createdAtRuntime: false} +const publishedTheme = {...originalTheme, role: 'live'} +const store = 'test.myshopify.com' + +async function run(argv: string[]) { + const config = new Config({root: __dirname}) + await config.load() + vi.mocked(ensureAuthenticatedThemes).mockImplementation(async (storeFqdn) => ({token: 'token', storeFqdn})) + return runWithCommandEventsForCommand(argv, () => new Publish(argv, config).run()) +} + +async function inEnvironments(run: () => Promise) { + await inTemporaryDirectory(async (directory) => { + await writeFile( + joinPath(directory, 'shopify.theme.toml'), + ` +[environments.first] +store = "first.myshopify.com" +password = "token" +[environments.second] +store = "second.myshopify.com" +password = "token" +`, + ) + const cwd = vi.spyOn(process, 'cwd').mockReturnValue(directory) + try { + await run() + } finally { + cwd.mockRestore() + } + }) +} + +describe('theme publish JSON output', () => { + test('exposes its schema and JSON flag in help', () => { + expect(Publish.jsonOutputSchema).toBe(themePublishJsonOutputSchema) + expect(Publish.flags.json).toBeDefined() + expect(Publish.description).toContain('ThemePublishResult') + }) + + test.each([undefined, '', 'https://example.com/theme.zip'])( + 'returns the updated theme and omits missing src (%s)', + async (src) => { + vi.mocked(findOrSelectTheme).mockResolvedValue(originalTheme) + vi.mocked(themePublish).mockResolvedValue({...publishedTheme, src}) + await withCapturedStandardStreams(async ({stdout, stderr}) => { + await run(['--store', store, '--theme', '1', '--force', '--json']) + expect(JSON.parse(stdout())).toEqual({ + status: 'success', + theme: {...publishedTheme, ...(src === undefined ? {} : {src}), shop: store}, + }) + expect(stderr()).toBe('') + }) + }, + ) + + test('routes diagnostics to stderr', async () => { + vi.mocked(findOrSelectTheme).mockResolvedValue(originalTheme) + vi.mocked(themePublish).mockImplementation(async () => { + outputWarn('Retrying request') + return publishedTheme + }) + await withCapturedStandardStreams(async ({stdout, stderr}) => { + await run(['--store', store, '--theme', '1', '--force', '--json']) + expect(JSON.parse(stdout()).theme.role).toBe('live') + expect(JSON.parse(stderr())).toMatchObject({type: 'diagnostic', level: 'warning', message: 'Retrying request'}) + }) + }) + + test('leaves execution failures to the shared error handler without writing a result', async () => { + vi.mocked(findOrSelectTheme).mockResolvedValue(originalTheme) + vi.mocked(themePublish).mockRejectedValue(new Error('Publishing failed')) + await withCapturedStandardStreams(async ({stdout}) => { + await expect(run(['--store', store, '--theme', '1', '--force', '--json'])).rejects.toThrow('Publishing failed') + expect(stdout()).toBe('') + }) + }) + + test.each(['none', 'partial', 'all'])('collects one document in configured order (%s failures)', async (failures) => { + vi.mocked(findOrSelectTheme).mockResolvedValue(originalTheme) + vi.mocked(themePublish).mockImplementation(async (_id, session) => { + if (failures === 'all' || (failures === 'partial' && session.storeFqdn.startsWith('first.'))) { + throw new Error('Publishing failed') + } + return publishedTheme + }) + const exitCode = process.exitCode + await inEnvironments(async () => { + await withCapturedStandardStreams(async ({stdout, stderr}) => { + await run(['--environment', 'first', '--environment', 'second', '--theme', '1', '--force', '--json']) + const environments: string[] = [] + if (failures === 'none') environments.push('first') + if (failures !== 'all') environments.push('second') + expect(JSON.parse(stdout())).toEqual({ + environments: environments.map((environment) => ({ + environment, + result: {status: 'success', theme: {...publishedTheme, shop: `${environment}.myshopify.com`}}, + })), + }) + if (failures === 'none') { + expect(stderr()).toBe('') + } else { + const errors = stderr() + .trim() + .split('\n') + .map((line) => JSON.parse(line)) + expect(errors).toHaveLength(failures === 'all' ? 2 : 1) + expect(errors[0]).toMatchObject({type: 'diagnostic', level: 'error', code: 'theme-environment-failed'}) + } + expect(process.exitCode).toBe(exitCode) + }) + }) + }) + + test.each([ + {status: 'success', theme: {...publishedTheme, id: '1', shop: store}}, + {status: 'success', theme: {...publishedTheme, shop: null}}, + {status: 'success', theme: {...publishedTheme, src: false, shop: store}}, + { + environments: [ + {environment: 'first', result: {status: 'success', theme: {...publishedTheme, role: null, shop: store}}}, + ], + }, + ])('rejects malformed results %#', (result) => { + expect(() => themePublishJsonOutputSchema.validate(result)).toThrow() + }) +}) diff --git a/packages/theme/src/cli/commands/theme/publish.ts b/packages/theme/src/cli/commands/theme/publish.ts index 989b68829cd..c913c682433 100644 --- a/packages/theme/src/cli/commands/theme/publish.ts +++ b/packages/theme/src/cli/commands/theme/publish.ts @@ -1,14 +1,21 @@ +import {themePublishJsonOutputSchema} from '../../services/publish/types.js' +import {renderThemePublishResult} from '../../services/publish/result.js' import {publish} from '../../services/publish.js' import {themeFlags} from '../../flags.js' import ThemeCommand from '../../utilities/theme-command.js' +import {outputResult} from '@shopify/cli-kit/node/output' import {Flags} from '@oclif/core' -import {globalFlags, requiredIfNonInteractive} from '@shopify/cli-kit/node/cli' +import {globalFlags, jsonFlag, requiredIfNonInteractive} from '@shopify/cli-kit/node/cli' import {OutputFlags} from '@oclif/core/interfaces' import {AdminSession} from '@shopify/cli-kit/node/session' type PublishFlags = OutputFlags export default class Publish extends ThemeCommand { + static get jsonOutputSchema() { + return themePublishJsonOutputSchema + } + static summary = 'Set a remote theme as the live theme.' static descriptionWithMarkdown = `Publishes an unpublished theme from your theme library. @@ -23,6 +30,7 @@ If you want to publish your local theme, then you need to run \`shopify theme pu static flags = { ...globalFlags, + ...jsonFlag, ...themeFlags, force: requiredIfNonInteractive( Flags.boolean({ @@ -43,6 +51,17 @@ If you want to publish your local theme, then you need to run \`shopify theme pu static multiEnvironmentsFlags = ['store', 'password', 'theme'] async command(flags: PublishFlags, adminSession: AdminSession, multiEnvironment?: boolean) { - await publish(adminSession, flags, multiEnvironment) + const result = await publish(adminSession, flags, multiEnvironment) + if (!result) return + if (flags.json && multiEnvironment) return result.data + renderThemePublishResult(result, flags.json ? 'json' : 'text', flags.environment) + } + + protected collectsEnvironmentResults(flags: {json?: boolean}): boolean { + return Boolean(flags.json) + } + + protected renderEnvironmentResults(environments: {environment: string; result: unknown}[]): void { + outputResult(themePublishJsonOutputSchema.encode(themePublishJsonOutputSchema.validate({environments}))) } } diff --git a/packages/theme/src/cli/services/publish.test.ts b/packages/theme/src/cli/services/publish.test.ts index fe2af71f2e0..305fe05f5ae 100644 --- a/packages/theme/src/cli/services/publish.test.ts +++ b/packages/theme/src/cli/services/publish.test.ts @@ -1,4 +1,5 @@ -import {publish} from './publish.js' +import {publish as executePublish} from './publish.js' +import {renderThemePublishResult} from './publish/result.js' import {findOrSelectTheme} from '../utilities/theme-selector.js' import {renderSuccess, renderConfirmationPrompt} from '@shopify/cli-kit/node/ui' import {test, describe, expect, vi} from 'vitest' @@ -105,3 +106,48 @@ describe('publish', () => { }) }) }) + +async function publish(...args: Parameters) { + const result = await executePublish(...args) + if (result) renderThemePublishResult(result, 'text') + return result +} + +test('returns the published API data without presenting a final result', async () => { + const publishedTheme = {...theme, name: 'Published name', role: 'live', processing: false, createdAtRuntime: false} + vi.mocked(findOrSelectTheme).mockResolvedValue(theme) + vi.mocked(themePublish).mockResolvedValue(publishedTheme) + const result = await executePublish(session, {...options, force: true}) + expect(result).toEqual({ + data: {status: 'success', theme: {...publishedTheme, shop: session.storeFqdn}}, + originalTheme: theme, + previewUrl: 'https://my-shop.myshopify.com', + }) + expect(renderSuccess).not.toHaveBeenCalled() +}) + +test('skips confirmation when already confirmed for multiple environments', async () => { + vi.mocked(findOrSelectTheme).mockResolvedValue(theme) + vi.mocked(themePublish).mockResolvedValue(theme) + await executePublish(session, options, true) + expect(renderConfirmationPrompt).not.toHaveBeenCalled() + expect(themePublish).toHaveBeenCalledWith(1, session) +}) + +test('keeps the original theme name and environment label in terminal output', async () => { + vi.mocked(findOrSelectTheme).mockResolvedValue(theme) + vi.mocked(themePublish).mockResolvedValue({...theme, name: 'Canonical name', role: 'live'}) + const result = await executePublish(session, {...options, force: true}) + renderThemePublishResult(result!, 'text', ['staging']) + expect(renderSuccess).toHaveBeenCalledWith({ + headline: 'Environment: staging', + body: [ + 'The theme', + "'my theme'", + {subdued: '(#1)'}, + 'is now live at', + {link: {label: 'https://my-shop.myshopify.com', url: 'https://my-shop.myshopify.com'}}, + {char: '.'}, + ], + }) +}) diff --git a/packages/theme/src/cli/services/publish.ts b/packages/theme/src/cli/services/publish.ts index 008976ce45f..ba08560b471 100644 --- a/packages/theme/src/cli/services/publish.ts +++ b/packages/theme/src/cli/services/publish.ts @@ -1,18 +1,21 @@ -import {themeComponent} from '../utilities/theme-ui.js' +import {ThemePublishResult} from './publish/types.js' import {findOrSelectTheme} from '../utilities/theme-selector.js' import {themePublish} from '@shopify/cli-kit/node/themes/api' import {themePreviewUrl} from '@shopify/cli-kit/node/themes/urls' import {Theme} from '@shopify/cli-kit/node/themes/types' -import {renderConfirmationPrompt, renderSuccess} from '@shopify/cli-kit/node/ui' +import {renderConfirmationPrompt} from '@shopify/cli-kit/node/ui' import {AdminSession} from '@shopify/cli-kit/node/session' interface PublishServiceOptions { theme: string | undefined force: boolean - environment?: string } -export async function publish(adminSession: AdminSession, options: PublishServiceOptions, multiEnvironment?: boolean) { +export async function publish( + adminSession: AdminSession, + options: PublishServiceOptions, + multiEnvironment?: boolean, +): Promise { const themeToPublish = await findOrSelectTheme(adminSession, { header: 'Select a theme to publish', filter: { @@ -33,21 +36,11 @@ export async function publish(adminSession: AdminSession, options: PublishServic if (!accept) return } - await themePublish(themeToPublish.id, adminSession) - - renderSuccess({ - headline: options.environment ? `Environment: ${options.environment}` : undefined, - body: [ - 'The theme', - ...themeComponent(themeToPublish), - 'is now live at', - { - link: { - label: previewUrl, - url: previewUrl, - }, - }, - {char: '.'}, - ], - }) + // The API helper rejects missing themes and user errors before returning. + const publishedTheme = (await themePublish(themeToPublish.id, adminSession))! + return { + data: {status: 'success', theme: {...publishedTheme, shop: adminSession.storeFqdn}}, + originalTheme: themeToPublish, + previewUrl, + } } diff --git a/packages/theme/src/cli/services/publish/result.ts b/packages/theme/src/cli/services/publish/result.ts new file mode 100644 index 00000000000..8ea2a9b2506 --- /dev/null +++ b/packages/theme/src/cli/services/publish/result.ts @@ -0,0 +1,25 @@ +import {themePublishJsonOutputSchema, type ThemePublishResult} from './types.js' +import {themeComponent} from '../../utilities/theme-ui.js' +import {outputResult} from '@shopify/cli-kit/node/output' +import {renderSuccess} from '@shopify/cli-kit/node/ui' + +export function renderThemePublishResult( + result: ThemePublishResult, + format: 'text' | 'json', + environment?: string[], +): void { + if (format === 'json') { + outputResult(themePublishJsonOutputSchema.encode(result.data)) + return + } + renderSuccess({ + headline: environment ? `Environment: ${environment}` : undefined, + body: [ + 'The theme', + ...themeComponent(result.originalTheme), + 'is now live at', + {link: {label: result.previewUrl, url: result.previewUrl}}, + {char: '.'}, + ], + }) +} diff --git a/packages/theme/src/cli/services/publish/types.ts b/packages/theme/src/cli/services/publish/types.ts new file mode 100644 index 00000000000..bd76126096e --- /dev/null +++ b/packages/theme/src/cli/services/publish/types.ts @@ -0,0 +1,21 @@ +import {ThemeMutationSuccessSchema} from '../theme-mutation/status.js' +import {ThemeMutationThemeSchema} from '../theme-mutation/types.js' +import {defineJsonOutputSchema} from '@shopify/cli-kit/node/json-output-schema' +import {zod} from '@shopify/cli-kit/node/schema' +import type {Theme} from '@shopify/cli-kit/node/themes/types' + +const ThemePublishResultSchema = ThemeMutationSuccessSchema.extend({theme: ThemeMutationThemeSchema}) +const ThemePublishEnvironmentSchema = zod.object({environment: zod.string(), result: ThemePublishResultSchema}) + +export const themePublishJsonOutputSchema = defineJsonOutputSchema({ + name: 'ThemePublishResult', + schema: zod.union([ThemePublishResultSchema, zod.object({environments: zod.array(ThemePublishEnvironmentSchema)})]), + definitions: {PublishedTheme: ThemeMutationThemeSchema, ThemePublishEnvironment: ThemePublishEnvironmentSchema}, +}) + +export type ThemePublishData = zod.infer +export interface ThemePublishResult { + data: ThemePublishData + originalTheme: Theme + previewUrl: string +} From 01b3a1237aa63e74361f14779c08bac9ca677949 Mon Sep 17 00:00:00 2001 From: Gonzalo Riestra Date: Tue, 6 Oct 2026 14:06:25 +0200 Subject: [PATCH 2/5] Publish strict theme resource projections and explicit JSON cancellations --- packages/cli/README.md | 295 +++++++++++++++++- packages/cli/oclif.manifest.json | 2 +- .../src/cli/commands/theme/publish.test.ts | 29 +- .../theme/src/cli/commands/theme/publish.ts | 11 +- .../theme/src/cli/services/publish/types.ts | 26 +- 5 files changed, 342 insertions(+), 21 deletions(-) diff --git a/packages/cli/README.md b/packages/cli/README.md index 85a30c69f42..dae9bd7739e 100644 --- a/packages/cli/README.md +++ b/packages/cli/README.md @@ -14200,8 +14200,8 @@ Set a remote theme as the live theme. ``` USAGE - $ shopify theme publish [--auth-alias ] [-e ...] [-f] [--json-schema] [--no-color] [--no-input] - [--password ] [--path ] [-s ] [-t ] [--verbose] + $ shopify theme publish [--auth-alias ] [-e ...] [-f] [-j] [--json-schema] [--no-color] + [--no-input] [--password ] [--path ] [-s ] [-t ] [--verbose] FLAGS -e, --environment=... @@ -14212,6 +14212,10 @@ FLAGS Skip confirmation. Required if non interactive. [env: SHOPIFY_FLAG_FORCE] + -j, --json + Output the result as JSON. Automatically disables color output. + [env: SHOPIFY_FLAG_JSON] + -s, --store= Store URL. It can be the store prefix (example) or the full myshopify.com URL (example.myshopify.com, https://example.myshopify.com). @@ -14262,6 +14266,293 @@ DESCRIPTION If 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. + + Use `--json-schema` to print the result, error, and event schemas. + + Output from `--json` conforms to the `ThemePublishResult` schema. + + ```json + { + "anyOf": [ + { + "$ref": "#/definitions/ThemeEnvironment/anyOf/0/properties/result" + }, + { + "type": "object", + "properties": { + "environments": { + "type": "array", + "items": { + "$ref": "#/definitions/ThemeEnvironment" + } + } + }, + "required": [ + "environments" + ], + "additionalProperties": false + } + ], + "title": "ThemePublishResult", + "definitions": { + "Theme": { + "type": "object", + "properties": { + "id": { + "type": "string", + "pattern": "^\\d+$", + "description": "The decimal Online Store theme ID, not a Shopify GID." + }, + "name": { + "type": "string" + }, + "role": { + "type": "string", + "description": "The upstream theme role; known values include main, unpublished, and development." + }, + "storeDomain": { + "anyOf": [ + { + "type": "string", + "pattern": "^[a-z0-9][a-z0-9-]*\\.myshopify\\.com$" + }, + { + "type": "null" + } + ] + }, + "processing": { + "type": "boolean" + }, + "sourceUrl": { + "anyOf": [ + { + "type": "string", + "format": "uri" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "id", + "name", + "role", + "storeDomain", + "processing", + "sourceUrl" + ], + "additionalProperties": false, + "description": "The selected theme projection; sourceUrl is null when the upstream source URL is unavailable." + }, + "ThemeEnvironment": { + "anyOf": [ + { + "type": "object", + "properties": { + "environment": { + "type": "string" + }, + "result": { + "anyOf": [ + { + "type": "object", + "properties": { + "status": { + "type": "string", + "const": "success" + }, + "changed": { + "type": "boolean" + }, + "theme": { + "$ref": "#/definitions/Theme" + } + }, + "required": [ + "status", + "changed", + "theme" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "status": { + "type": "string", + "const": "cancelled" + } + }, + "required": [ + "status" + ], + "additionalProperties": false + } + ] + } + }, + "required": [ + "environment", + "result" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "environment": { + "type": "string" + }, + "error": { + "anyOf": [ + { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "abort" + }, + "message": { + "type": "string" + }, + "tryMessage": { + "type": "string" + }, + "nextSteps": { + "type": "array", + "items": { + "type": "string" + } + }, + "customSections": { + "type": "array", + "items": { + "type": "object", + "properties": { + "title": { + "type": "string" + }, + "body": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "array", + "items": { + "type": "array", + "items": { + "type": "string" + } + } + } + ] + } + }, + "required": [ + "body" + ], + "additionalProperties": false + } + }, + "details": {} + }, + "required": [ + "type", + "message" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "bug" + }, + "message": { + "$ref": "#/definitions/ThemeEnvironment/anyOf/1/properties/error/anyOf/0/properties/message" + }, + "tryMessage": { + "$ref": "#/definitions/ThemeEnvironment/anyOf/1/properties/error/anyOf/0/properties/tryMessage" + }, + "nextSteps": { + "$ref": "#/definitions/ThemeEnvironment/anyOf/1/properties/error/anyOf/0/properties/nextSteps" + }, + "customSections": { + "$ref": "#/definitions/ThemeEnvironment/anyOf/1/properties/error/anyOf/0/properties/customSections" + }, + "details": { + "$ref": "#/definitions/ThemeEnvironment/anyOf/1/properties/error/anyOf/0/properties/details" + }, + "stack": { + "type": "string" + } + }, + "required": [ + "type", + "message" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "external" + }, + "message": { + "$ref": "#/definitions/ThemeEnvironment/anyOf/1/properties/error/anyOf/0/properties/message" + }, + "tryMessage": { + "$ref": "#/definitions/ThemeEnvironment/anyOf/1/properties/error/anyOf/0/properties/tryMessage" + }, + "nextSteps": { + "$ref": "#/definitions/ThemeEnvironment/anyOf/1/properties/error/anyOf/0/properties/nextSteps" + }, + "customSections": { + "$ref": "#/definitions/ThemeEnvironment/anyOf/1/properties/error/anyOf/0/properties/customSections" + }, + "details": { + "$ref": "#/definitions/ThemeEnvironment/anyOf/1/properties/error/anyOf/0/properties/details" + }, + "command": { + "type": "string" + }, + "args": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": [ + "type", + "message", + "command", + "args" + ], + "additionalProperties": false + } + ] + } + }, + "required": [ + "environment", + "error" + ], + "additionalProperties": false + } + ] + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" + } + ``` ``` ## `shopify theme pull` diff --git a/packages/cli/oclif.manifest.json b/packages/cli/oclif.manifest.json index d714a4e6401..545690e2aae 100644 --- a/packages/cli/oclif.manifest.json +++ b/packages/cli/oclif.manifest.json @@ -12331,7 +12331,7 @@ "args": { }, "customPluginName": "@shopify/theme", - "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\nUse `--json-schema` to print the result, error, and event schemas.\n\nOutput from `--json` conforms to the `ThemePublishResult` schema.\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 \"status\": {\n \"type\": \"string\",\n \"const\": \"success\"\n },\n \"theme\": {\n \"$ref\": \"#/definitions/PublishedTheme\"\n }\n },\n \"required\": [\n \"status\",\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```", + "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\nUse `--json-schema` to print the result, error, and event schemas.\n\nOutput from `--json` conforms to the `ThemePublishResult` schema.\n\n```json\n{\n \"anyOf\": [\n {\n \"$ref\": \"#/definitions/ThemeEnvironment/anyOf/0/properties/result\"\n },\n {\n \"type\": \"object\",\n \"properties\": {\n \"environments\": {\n \"type\": \"array\",\n \"items\": {\n \"$ref\": \"#/definitions/ThemeEnvironment\"\n }\n }\n },\n \"required\": [\n \"environments\"\n ],\n \"additionalProperties\": false\n }\n ],\n \"title\": \"ThemePublishResult\",\n \"definitions\": {\n \"Theme\": {\n \"type\": \"object\",\n \"properties\": {\n \"id\": {\n \"type\": \"string\",\n \"pattern\": \"^\\\\d+$\",\n \"description\": \"The decimal Online Store theme ID, not a Shopify GID.\"\n },\n \"name\": {\n \"type\": \"string\"\n },\n \"role\": {\n \"type\": \"string\",\n \"description\": \"The upstream theme role; known values include main, unpublished, and development.\"\n },\n \"storeDomain\": {\n \"anyOf\": [\n {\n \"type\": \"string\",\n \"pattern\": \"^[a-z0-9][a-z0-9-]*\\\\.myshopify\\\\.com$\"\n },\n {\n \"type\": \"null\"\n }\n ]\n },\n \"processing\": {\n \"type\": \"boolean\"\n },\n \"sourceUrl\": {\n \"anyOf\": [\n {\n \"type\": \"string\",\n \"format\": \"uri\"\n },\n {\n \"type\": \"null\"\n }\n ]\n }\n },\n \"required\": [\n \"id\",\n \"name\",\n \"role\",\n \"storeDomain\",\n \"processing\",\n \"sourceUrl\"\n ],\n \"additionalProperties\": false,\n \"description\": \"The selected theme projection; sourceUrl is null when the upstream source URL is unavailable.\"\n },\n \"ThemeEnvironment\": {\n \"anyOf\": [\n {\n \"type\": \"object\",\n \"properties\": {\n \"environment\": {\n \"type\": \"string\"\n },\n \"result\": {\n \"anyOf\": [\n {\n \"type\": \"object\",\n \"properties\": {\n \"status\": {\n \"type\": \"string\",\n \"const\": \"success\"\n },\n \"changed\": {\n \"type\": \"boolean\"\n },\n \"theme\": {\n \"$ref\": \"#/definitions/Theme\"\n }\n },\n \"required\": [\n \"status\",\n \"changed\",\n \"theme\"\n ],\n \"additionalProperties\": false\n },\n {\n \"type\": \"object\",\n \"properties\": {\n \"status\": {\n \"type\": \"string\",\n \"const\": \"cancelled\"\n }\n },\n \"required\": [\n \"status\"\n ],\n \"additionalProperties\": false\n }\n ]\n }\n },\n \"required\": [\n \"environment\",\n \"result\"\n ],\n \"additionalProperties\": false\n },\n {\n \"type\": \"object\",\n \"properties\": {\n \"environment\": {\n \"type\": \"string\"\n },\n \"error\": {\n \"anyOf\": [\n {\n \"type\": \"object\",\n \"properties\": {\n \"type\": {\n \"type\": \"string\",\n \"const\": \"abort\"\n },\n \"message\": {\n \"type\": \"string\"\n },\n \"tryMessage\": {\n \"type\": \"string\"\n },\n \"nextSteps\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\"\n }\n },\n \"customSections\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"properties\": {\n \"title\": {\n \"type\": \"string\"\n },\n \"body\": {\n \"anyOf\": [\n {\n \"type\": \"string\"\n },\n {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\"\n }\n }\n }\n ]\n }\n },\n \"required\": [\n \"body\"\n ],\n \"additionalProperties\": false\n }\n },\n \"details\": {}\n },\n \"required\": [\n \"type\",\n \"message\"\n ],\n \"additionalProperties\": false\n },\n {\n \"type\": \"object\",\n \"properties\": {\n \"type\": {\n \"type\": \"string\",\n \"const\": \"bug\"\n },\n \"message\": {\n \"$ref\": \"#/definitions/ThemeEnvironment/anyOf/1/properties/error/anyOf/0/properties/message\"\n },\n \"tryMessage\": {\n \"$ref\": \"#/definitions/ThemeEnvironment/anyOf/1/properties/error/anyOf/0/properties/tryMessage\"\n },\n \"nextSteps\": {\n \"$ref\": \"#/definitions/ThemeEnvironment/anyOf/1/properties/error/anyOf/0/properties/nextSteps\"\n },\n \"customSections\": {\n \"$ref\": \"#/definitions/ThemeEnvironment/anyOf/1/properties/error/anyOf/0/properties/customSections\"\n },\n \"details\": {\n \"$ref\": \"#/definitions/ThemeEnvironment/anyOf/1/properties/error/anyOf/0/properties/details\"\n },\n \"stack\": {\n \"type\": \"string\"\n }\n },\n \"required\": [\n \"type\",\n \"message\"\n ],\n \"additionalProperties\": false\n },\n {\n \"type\": \"object\",\n \"properties\": {\n \"type\": {\n \"type\": \"string\",\n \"const\": \"external\"\n },\n \"message\": {\n \"$ref\": \"#/definitions/ThemeEnvironment/anyOf/1/properties/error/anyOf/0/properties/message\"\n },\n \"tryMessage\": {\n \"$ref\": \"#/definitions/ThemeEnvironment/anyOf/1/properties/error/anyOf/0/properties/tryMessage\"\n },\n \"nextSteps\": {\n \"$ref\": \"#/definitions/ThemeEnvironment/anyOf/1/properties/error/anyOf/0/properties/nextSteps\"\n },\n \"customSections\": {\n \"$ref\": \"#/definitions/ThemeEnvironment/anyOf/1/properties/error/anyOf/0/properties/customSections\"\n },\n \"details\": {\n \"$ref\": \"#/definitions/ThemeEnvironment/anyOf/1/properties/error/anyOf/0/properties/details\"\n },\n \"command\": {\n \"type\": \"string\"\n },\n \"args\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\"\n }\n }\n },\n \"required\": [\n \"type\",\n \"message\",\n \"command\",\n \"args\"\n ],\n \"additionalProperties\": false\n }\n ]\n }\n },\n \"required\": [\n \"environment\",\n \"error\"\n ],\n \"additionalProperties\": false\n }\n ]\n }\n },\n \"$schema\": \"http://json-schema.org/draft-07/schema#\"\n}\n```", "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.", "enableJsonFlag": false, "flags": { diff --git a/packages/theme/src/cli/commands/theme/publish.test.ts b/packages/theme/src/cli/commands/theme/publish.test.ts index 8dcd04ade73..51c27ce52ec 100644 --- a/packages/theme/src/cli/commands/theme/publish.test.ts +++ b/packages/theme/src/cli/commands/theme/publish.test.ts @@ -20,6 +20,11 @@ vi.mock('../../utilities/theme-store.js', () => ({ensureThemeStore: ({store}: {s const originalTheme = {id: 1, name: 'Original', role: 'unpublished', processing: false, createdAtRuntime: false} const publishedTheme = {...originalTheme, role: 'live'} const store = 'test.myshopify.com' +const publicTheme = {id: '1', name: 'Original', role: 'live', processing: false, storeDomain: store, sourceUrl: null} + +function restoreExitCode(value: typeof process.exitCode): void { + process.exitCode = value +} async function run(argv: string[]) { const config = new Config({root: __dirname}) @@ -58,7 +63,7 @@ describe('theme publish JSON output', () => { }) test.each([undefined, '', 'https://example.com/theme.zip'])( - 'returns the updated theme and omits missing src (%s)', + 'returns the updated theme and nullable source URL (%s)', async (src) => { vi.mocked(findOrSelectTheme).mockResolvedValue(originalTheme) vi.mocked(themePublish).mockResolvedValue({...publishedTheme, src}) @@ -66,7 +71,8 @@ describe('theme publish JSON output', () => { await run(['--store', store, '--theme', '1', '--force', '--json']) expect(JSON.parse(stdout())).toEqual({ status: 'success', - theme: {...publishedTheme, ...(src === undefined ? {} : {src}), shop: store}, + changed: true, + theme: {...publicTheme, sourceUrl: src === '' ? null : (src ?? null)}, }) expect(stderr()).toBe('') }) @@ -111,10 +117,18 @@ describe('theme publish JSON output', () => { if (failures === 'none') environments.push('first') if (failures !== 'all') environments.push('second') expect(JSON.parse(stdout())).toEqual({ - environments: environments.map((environment) => ({ - environment, - result: {status: 'success', theme: {...publishedTheme, shop: `${environment}.myshopify.com`}}, - })), + environments: ['first', 'second'].map((environment) => + environments.includes(environment) + ? { + environment, + result: { + status: 'success', + changed: true, + theme: {...publicTheme, storeDomain: `${environment}.myshopify.com`}, + }, + } + : {environment, error: {type: 'abort', message: 'Publishing failed'}}, + ), }) if (failures === 'none') { expect(stderr()).toBe('') @@ -126,7 +140,8 @@ describe('theme publish JSON output', () => { expect(errors).toHaveLength(failures === 'all' ? 2 : 1) expect(errors[0]).toMatchObject({type: 'diagnostic', level: 'error', code: 'theme-environment-failed'}) } - expect(process.exitCode).toBe(exitCode) + expect(process.exitCode).toBe(failures === 'none' ? exitCode : 1) + restoreExitCode(exitCode) }) }) }) diff --git a/packages/theme/src/cli/commands/theme/publish.ts b/packages/theme/src/cli/commands/theme/publish.ts index c913c682433..ca0689d8473 100644 --- a/packages/theme/src/cli/commands/theme/publish.ts +++ b/packages/theme/src/cli/commands/theme/publish.ts @@ -52,7 +52,10 @@ If you want to publish your local theme, then you need to run \`shopify theme pu async command(flags: PublishFlags, adminSession: AdminSession, multiEnvironment?: boolean) { const result = await publish(adminSession, flags, multiEnvironment) - if (!result) return + if (!result) { + if (flags.json && !multiEnvironment) outputResult(themePublishJsonOutputSchema.encode({status: 'cancelled'})) + return + } if (flags.json && multiEnvironment) return result.data renderThemePublishResult(result, flags.json ? 'json' : 'text', flags.environment) } @@ -61,7 +64,9 @@ If you want to publish your local theme, then you need to run \`shopify theme pu return Boolean(flags.json) } - protected renderEnvironmentResults(environments: {environment: string; result: unknown}[]): void { - outputResult(themePublishJsonOutputSchema.encode(themePublishJsonOutputSchema.validate({environments}))) + protected renderEnvironmentResults( + environments: import('../../services/json-output/schema.js').ThemeEnvironmentResult[], + ): void { + outputResult(themePublishJsonOutputSchema.encode({environments})) } } diff --git a/packages/theme/src/cli/services/publish/types.ts b/packages/theme/src/cli/services/publish/types.ts index bd76126096e..1a259306ecd 100644 --- a/packages/theme/src/cli/services/publish/types.ts +++ b/packages/theme/src/cli/services/publish/types.ts @@ -1,16 +1,26 @@ import {ThemeMutationSuccessSchema} from '../theme-mutation/status.js' -import {ThemeMutationThemeSchema} from '../theme-mutation/types.js' -import {defineJsonOutputSchema} from '@shopify/cli-kit/node/json-output-schema' +import { + ThemeMutationThemeSchema, + ThemeMutationJsonThemeSchema, + projectThemeMutationTheme, +} from '../theme-mutation/types.js' +import {defineThemeJsonOutputSchema} from '../json-output/schema.js' import {zod} from '@shopify/cli-kit/node/schema' import type {Theme} from '@shopify/cli-kit/node/themes/types' -const ThemePublishResultSchema = ThemeMutationSuccessSchema.extend({theme: ThemeMutationThemeSchema}) -const ThemePublishEnvironmentSchema = zod.object({environment: zod.string(), result: ThemePublishResultSchema}) - -export const themePublishJsonOutputSchema = defineJsonOutputSchema({ +const ThemePublishResultSchema = ThemeMutationSuccessSchema.extend({ + theme: ThemeMutationThemeSchema, +}) +export const themePublishJsonOutputSchema = defineThemeJsonOutputSchema({ name: 'ThemePublishResult', - schema: zod.union([ThemePublishResultSchema, zod.object({environments: zod.array(ThemePublishEnvironmentSchema)})]), - definitions: {PublishedTheme: ThemeMutationThemeSchema, ThemePublishEnvironment: ThemePublishEnvironmentSchema}, + schema: zod + .object({status: zod.literal('success'), changed: zod.boolean(), theme: ThemeMutationJsonThemeSchema}) + .strict(), + definitions: {Theme: ThemeMutationJsonThemeSchema}, + project(value) { + const result = ThemePublishResultSchema.parse(value) + return {status: result.status, changed: true, theme: projectThemeMutationTheme(result.theme)} + }, }) export type ThemePublishData = zod.infer From 1195e2d091c17d444176a7d8467bb3fb28df4209 Mon Sep 17 00:00:00 2001 From: Gonzalo Riestra Date: Tue, 6 Oct 2026 14:16:41 +0200 Subject: [PATCH 3/5] Refresh theme publish JSON schema documentation --- packages/cli/README.md | 2 +- packages/cli/oclif.manifest.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/cli/README.md b/packages/cli/README.md index dae9bd7739e..131bb880a27 100644 --- a/packages/cli/README.md +++ b/packages/cli/README.md @@ -14308,7 +14308,7 @@ DESCRIPTION }, "role": { "type": "string", - "description": "The upstream theme role; known values include main, unpublished, and development." + "description": "The upstream theme role; known values include live, unpublished, and development." }, "storeDomain": { "anyOf": [ diff --git a/packages/cli/oclif.manifest.json b/packages/cli/oclif.manifest.json index 545690e2aae..6a1e866c5f0 100644 --- a/packages/cli/oclif.manifest.json +++ b/packages/cli/oclif.manifest.json @@ -12331,7 +12331,7 @@ "args": { }, "customPluginName": "@shopify/theme", - "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\nUse `--json-schema` to print the result, error, and event schemas.\n\nOutput from `--json` conforms to the `ThemePublishResult` schema.\n\n```json\n{\n \"anyOf\": [\n {\n \"$ref\": \"#/definitions/ThemeEnvironment/anyOf/0/properties/result\"\n },\n {\n \"type\": \"object\",\n \"properties\": {\n \"environments\": {\n \"type\": \"array\",\n \"items\": {\n \"$ref\": \"#/definitions/ThemeEnvironment\"\n }\n }\n },\n \"required\": [\n \"environments\"\n ],\n \"additionalProperties\": false\n }\n ],\n \"title\": \"ThemePublishResult\",\n \"definitions\": {\n \"Theme\": {\n \"type\": \"object\",\n \"properties\": {\n \"id\": {\n \"type\": \"string\",\n \"pattern\": \"^\\\\d+$\",\n \"description\": \"The decimal Online Store theme ID, not a Shopify GID.\"\n },\n \"name\": {\n \"type\": \"string\"\n },\n \"role\": {\n \"type\": \"string\",\n \"description\": \"The upstream theme role; known values include main, unpublished, and development.\"\n },\n \"storeDomain\": {\n \"anyOf\": [\n {\n \"type\": \"string\",\n \"pattern\": \"^[a-z0-9][a-z0-9-]*\\\\.myshopify\\\\.com$\"\n },\n {\n \"type\": \"null\"\n }\n ]\n },\n \"processing\": {\n \"type\": \"boolean\"\n },\n \"sourceUrl\": {\n \"anyOf\": [\n {\n \"type\": \"string\",\n \"format\": \"uri\"\n },\n {\n \"type\": \"null\"\n }\n ]\n }\n },\n \"required\": [\n \"id\",\n \"name\",\n \"role\",\n \"storeDomain\",\n \"processing\",\n \"sourceUrl\"\n ],\n \"additionalProperties\": false,\n \"description\": \"The selected theme projection; sourceUrl is null when the upstream source URL is unavailable.\"\n },\n \"ThemeEnvironment\": {\n \"anyOf\": [\n {\n \"type\": \"object\",\n \"properties\": {\n \"environment\": {\n \"type\": \"string\"\n },\n \"result\": {\n \"anyOf\": [\n {\n \"type\": \"object\",\n \"properties\": {\n \"status\": {\n \"type\": \"string\",\n \"const\": \"success\"\n },\n \"changed\": {\n \"type\": \"boolean\"\n },\n \"theme\": {\n \"$ref\": \"#/definitions/Theme\"\n }\n },\n \"required\": [\n \"status\",\n \"changed\",\n \"theme\"\n ],\n \"additionalProperties\": false\n },\n {\n \"type\": \"object\",\n \"properties\": {\n \"status\": {\n \"type\": \"string\",\n \"const\": \"cancelled\"\n }\n },\n \"required\": [\n \"status\"\n ],\n \"additionalProperties\": false\n }\n ]\n }\n },\n \"required\": [\n \"environment\",\n \"result\"\n ],\n \"additionalProperties\": false\n },\n {\n \"type\": \"object\",\n \"properties\": {\n \"environment\": {\n \"type\": \"string\"\n },\n \"error\": {\n \"anyOf\": [\n {\n \"type\": \"object\",\n \"properties\": {\n \"type\": {\n \"type\": \"string\",\n \"const\": \"abort\"\n },\n \"message\": {\n \"type\": \"string\"\n },\n \"tryMessage\": {\n \"type\": \"string\"\n },\n \"nextSteps\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\"\n }\n },\n \"customSections\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"properties\": {\n \"title\": {\n \"type\": \"string\"\n },\n \"body\": {\n \"anyOf\": [\n {\n \"type\": \"string\"\n },\n {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\"\n }\n }\n }\n ]\n }\n },\n \"required\": [\n \"body\"\n ],\n \"additionalProperties\": false\n }\n },\n \"details\": {}\n },\n \"required\": [\n \"type\",\n \"message\"\n ],\n \"additionalProperties\": false\n },\n {\n \"type\": \"object\",\n \"properties\": {\n \"type\": {\n \"type\": \"string\",\n \"const\": \"bug\"\n },\n \"message\": {\n \"$ref\": \"#/definitions/ThemeEnvironment/anyOf/1/properties/error/anyOf/0/properties/message\"\n },\n \"tryMessage\": {\n \"$ref\": \"#/definitions/ThemeEnvironment/anyOf/1/properties/error/anyOf/0/properties/tryMessage\"\n },\n \"nextSteps\": {\n \"$ref\": \"#/definitions/ThemeEnvironment/anyOf/1/properties/error/anyOf/0/properties/nextSteps\"\n },\n \"customSections\": {\n \"$ref\": \"#/definitions/ThemeEnvironment/anyOf/1/properties/error/anyOf/0/properties/customSections\"\n },\n \"details\": {\n \"$ref\": \"#/definitions/ThemeEnvironment/anyOf/1/properties/error/anyOf/0/properties/details\"\n },\n \"stack\": {\n \"type\": \"string\"\n }\n },\n \"required\": [\n \"type\",\n \"message\"\n ],\n \"additionalProperties\": false\n },\n {\n \"type\": \"object\",\n \"properties\": {\n \"type\": {\n \"type\": \"string\",\n \"const\": \"external\"\n },\n \"message\": {\n \"$ref\": \"#/definitions/ThemeEnvironment/anyOf/1/properties/error/anyOf/0/properties/message\"\n },\n \"tryMessage\": {\n \"$ref\": \"#/definitions/ThemeEnvironment/anyOf/1/properties/error/anyOf/0/properties/tryMessage\"\n },\n \"nextSteps\": {\n \"$ref\": \"#/definitions/ThemeEnvironment/anyOf/1/properties/error/anyOf/0/properties/nextSteps\"\n },\n \"customSections\": {\n \"$ref\": \"#/definitions/ThemeEnvironment/anyOf/1/properties/error/anyOf/0/properties/customSections\"\n },\n \"details\": {\n \"$ref\": \"#/definitions/ThemeEnvironment/anyOf/1/properties/error/anyOf/0/properties/details\"\n },\n \"command\": {\n \"type\": \"string\"\n },\n \"args\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\"\n }\n }\n },\n \"required\": [\n \"type\",\n \"message\",\n \"command\",\n \"args\"\n ],\n \"additionalProperties\": false\n }\n ]\n }\n },\n \"required\": [\n \"environment\",\n \"error\"\n ],\n \"additionalProperties\": false\n }\n ]\n }\n },\n \"$schema\": \"http://json-schema.org/draft-07/schema#\"\n}\n```", + "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\nUse `--json-schema` to print the result, error, and event schemas.\n\nOutput from `--json` conforms to the `ThemePublishResult` schema.\n\n```json\n{\n \"anyOf\": [\n {\n \"$ref\": \"#/definitions/ThemeEnvironment/anyOf/0/properties/result\"\n },\n {\n \"type\": \"object\",\n \"properties\": {\n \"environments\": {\n \"type\": \"array\",\n \"items\": {\n \"$ref\": \"#/definitions/ThemeEnvironment\"\n }\n }\n },\n \"required\": [\n \"environments\"\n ],\n \"additionalProperties\": false\n }\n ],\n \"title\": \"ThemePublishResult\",\n \"definitions\": {\n \"Theme\": {\n \"type\": \"object\",\n \"properties\": {\n \"id\": {\n \"type\": \"string\",\n \"pattern\": \"^\\\\d+$\",\n \"description\": \"The decimal Online Store theme ID, not a Shopify GID.\"\n },\n \"name\": {\n \"type\": \"string\"\n },\n \"role\": {\n \"type\": \"string\",\n \"description\": \"The upstream theme role; known values include live, unpublished, and development.\"\n },\n \"storeDomain\": {\n \"anyOf\": [\n {\n \"type\": \"string\",\n \"pattern\": \"^[a-z0-9][a-z0-9-]*\\\\.myshopify\\\\.com$\"\n },\n {\n \"type\": \"null\"\n }\n ]\n },\n \"processing\": {\n \"type\": \"boolean\"\n },\n \"sourceUrl\": {\n \"anyOf\": [\n {\n \"type\": \"string\",\n \"format\": \"uri\"\n },\n {\n \"type\": \"null\"\n }\n ]\n }\n },\n \"required\": [\n \"id\",\n \"name\",\n \"role\",\n \"storeDomain\",\n \"processing\",\n \"sourceUrl\"\n ],\n \"additionalProperties\": false,\n \"description\": \"The selected theme projection; sourceUrl is null when the upstream source URL is unavailable.\"\n },\n \"ThemeEnvironment\": {\n \"anyOf\": [\n {\n \"type\": \"object\",\n \"properties\": {\n \"environment\": {\n \"type\": \"string\"\n },\n \"result\": {\n \"anyOf\": [\n {\n \"type\": \"object\",\n \"properties\": {\n \"status\": {\n \"type\": \"string\",\n \"const\": \"success\"\n },\n \"changed\": {\n \"type\": \"boolean\"\n },\n \"theme\": {\n \"$ref\": \"#/definitions/Theme\"\n }\n },\n \"required\": [\n \"status\",\n \"changed\",\n \"theme\"\n ],\n \"additionalProperties\": false\n },\n {\n \"type\": \"object\",\n \"properties\": {\n \"status\": {\n \"type\": \"string\",\n \"const\": \"cancelled\"\n }\n },\n \"required\": [\n \"status\"\n ],\n \"additionalProperties\": false\n }\n ]\n }\n },\n \"required\": [\n \"environment\",\n \"result\"\n ],\n \"additionalProperties\": false\n },\n {\n \"type\": \"object\",\n \"properties\": {\n \"environment\": {\n \"type\": \"string\"\n },\n \"error\": {\n \"anyOf\": [\n {\n \"type\": \"object\",\n \"properties\": {\n \"type\": {\n \"type\": \"string\",\n \"const\": \"abort\"\n },\n \"message\": {\n \"type\": \"string\"\n },\n \"tryMessage\": {\n \"type\": \"string\"\n },\n \"nextSteps\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\"\n }\n },\n \"customSections\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"properties\": {\n \"title\": {\n \"type\": \"string\"\n },\n \"body\": {\n \"anyOf\": [\n {\n \"type\": \"string\"\n },\n {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\"\n }\n }\n }\n ]\n }\n },\n \"required\": [\n \"body\"\n ],\n \"additionalProperties\": false\n }\n },\n \"details\": {}\n },\n \"required\": [\n \"type\",\n \"message\"\n ],\n \"additionalProperties\": false\n },\n {\n \"type\": \"object\",\n \"properties\": {\n \"type\": {\n \"type\": \"string\",\n \"const\": \"bug\"\n },\n \"message\": {\n \"$ref\": \"#/definitions/ThemeEnvironment/anyOf/1/properties/error/anyOf/0/properties/message\"\n },\n \"tryMessage\": {\n \"$ref\": \"#/definitions/ThemeEnvironment/anyOf/1/properties/error/anyOf/0/properties/tryMessage\"\n },\n \"nextSteps\": {\n \"$ref\": \"#/definitions/ThemeEnvironment/anyOf/1/properties/error/anyOf/0/properties/nextSteps\"\n },\n \"customSections\": {\n \"$ref\": \"#/definitions/ThemeEnvironment/anyOf/1/properties/error/anyOf/0/properties/customSections\"\n },\n \"details\": {\n \"$ref\": \"#/definitions/ThemeEnvironment/anyOf/1/properties/error/anyOf/0/properties/details\"\n },\n \"stack\": {\n \"type\": \"string\"\n }\n },\n \"required\": [\n \"type\",\n \"message\"\n ],\n \"additionalProperties\": false\n },\n {\n \"type\": \"object\",\n \"properties\": {\n \"type\": {\n \"type\": \"string\",\n \"const\": \"external\"\n },\n \"message\": {\n \"$ref\": \"#/definitions/ThemeEnvironment/anyOf/1/properties/error/anyOf/0/properties/message\"\n },\n \"tryMessage\": {\n \"$ref\": \"#/definitions/ThemeEnvironment/anyOf/1/properties/error/anyOf/0/properties/tryMessage\"\n },\n \"nextSteps\": {\n \"$ref\": \"#/definitions/ThemeEnvironment/anyOf/1/properties/error/anyOf/0/properties/nextSteps\"\n },\n \"customSections\": {\n \"$ref\": \"#/definitions/ThemeEnvironment/anyOf/1/properties/error/anyOf/0/properties/customSections\"\n },\n \"details\": {\n \"$ref\": \"#/definitions/ThemeEnvironment/anyOf/1/properties/error/anyOf/0/properties/details\"\n },\n \"command\": {\n \"type\": \"string\"\n },\n \"args\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\"\n }\n }\n },\n \"required\": [\n \"type\",\n \"message\",\n \"command\",\n \"args\"\n ],\n \"additionalProperties\": false\n }\n ]\n }\n },\n \"required\": [\n \"environment\",\n \"error\"\n ],\n \"additionalProperties\": false\n }\n ]\n }\n },\n \"$schema\": \"http://json-schema.org/draft-07/schema#\"\n}\n```", "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.", "enableJsonFlag": false, "flags": { From 6692b3143d9258d6c25257ea6f0767aee5f17f5a Mon Sep 17 00:00:00 2001 From: Gonzalo Riestra Date: Tue, 6 Oct 2026 14:17:12 +0200 Subject: [PATCH 4/5] Refresh generated theme publish command docs --- .../generated/generated_docs_data_v2.json | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/docs-shopify.dev/generated/generated_docs_data_v2.json b/docs-shopify.dev/generated/generated_docs_data_v2.json index 71997a7d9fe..3fe36ca6378 100644 --- a/docs-shopify.dev/generated/generated_docs_data_v2.json +++ b/docs-shopify.dev/generated/generated_docs_data_v2.json @@ -10082,6 +10082,15 @@ "isOptional": true, "environmentValue": "SHOPIFY_FLAG_FORCE" }, + { + "filePath": "docs-shopify.dev/commands/interfaces/theme-publish.interface.ts", + "syntaxKind": "PropertySignature", + "name": "-j, --json", + "value": "''", + "description": "Output the result as JSON. Automatically disables color output.", + "isOptional": true, + "environmentValue": "SHOPIFY_FLAG_JSON" + }, { "filePath": "docs-shopify.dev/commands/interfaces/theme-publish.interface.ts", "syntaxKind": "PropertySignature", @@ -10101,7 +10110,7 @@ "environmentValue": "SHOPIFY_FLAG_THEME_ID" } ], - "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 '?: string\n\n /**\n * The environment to apply to the current command.\n * @environment SHOPIFY_FLAG_ENVIRONMENT\n */\n '-e, --environment '?: 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 * Disable interactive prompts and browser authentication.\n * @environment SHOPIFY_FLAG_NO_INPUT\n */\n '--no-input'?: ''\n\n /**\n * Password generated from the Theme Access app or an Admin API token.\n * @environment SHOPIFY_CLI_THEME_TOKEN\n */\n '--password '?: 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 '?: 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 '?: 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 '?: string\n\n /**\n * Increase the verbosity of the output. May include sensitive data.\n * @environment SHOPIFY_FLAG_VERBOSE\n */\n '--verbose'?: ''\n}" + "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 '?: string\n\n /**\n * The environment to apply to the current command.\n * @environment SHOPIFY_FLAG_ENVIRONMENT\n */\n '-e, --environment '?: 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 * Disable interactive prompts and browser authentication.\n * @environment SHOPIFY_FLAG_NO_INPUT\n */\n '--no-input'?: ''\n\n /**\n * Password generated from the Theme Access app or an Admin API token.\n * @environment SHOPIFY_CLI_THEME_TOKEN\n */\n '--password '?: 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 '?: 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 '?: 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 '?: string\n\n /**\n * Increase the verbosity of the output. May include sensitive data.\n * @environment SHOPIFY_FLAG_VERBOSE\n */\n '--verbose'?: ''\n}" } }, "themepull": { From 8056f087b27dcc06bf725635f643edc2cb19ec14 Mon Sep 17 00:00:00 2001 From: Gonzalo Riestra Date: Thu, 8 Oct 2026 10:36:42 +0200 Subject: [PATCH 5/5] Skip publishing live themes and refresh JSON schema documentation --- packages/cli/README.md | 15 ++++++++++- packages/cli/oclif.manifest.json | 2 +- .../src/cli/commands/theme/publish.test.ts | 23 ++++++++++++---- .../theme/src/cli/commands/theme/publish.ts | 4 --- .../theme/src/cli/services/publish.test.ts | 27 ++++++++++++++++++- packages/theme/src/cli/services/publish.ts | 10 ++++++- .../theme/src/cli/services/publish/result.ts | 2 +- .../theme/src/cli/services/publish/types.ts | 3 ++- 8 files changed, 71 insertions(+), 15 deletions(-) diff --git a/packages/cli/README.md b/packages/cli/README.md index 131bb880a27..b62d92f4678 100644 --- a/packages/cli/README.md +++ b/packages/cli/README.md @@ -14418,6 +14418,11 @@ DESCRIPTION "message": { "type": "string" }, + "code": { + "type": "string", + "minLength": 1, + "description": "A stable error code, included only when known." + }, "tryMessage": { "type": "string" }, @@ -14458,7 +14463,9 @@ DESCRIPTION "additionalProperties": false } }, - "details": {} + "details": { + "description": "Selected domain details, preserving native API payloads such as GraphQL errors, extensions, and data." + } }, "required": [ "type", @@ -14476,6 +14483,9 @@ DESCRIPTION "message": { "$ref": "#/definitions/ThemeEnvironment/anyOf/1/properties/error/anyOf/0/properties/message" }, + "code": { + "$ref": "#/definitions/ThemeEnvironment/anyOf/1/properties/error/anyOf/0/properties/code" + }, "tryMessage": { "$ref": "#/definitions/ThemeEnvironment/anyOf/1/properties/error/anyOf/0/properties/tryMessage" }, @@ -14508,6 +14518,9 @@ DESCRIPTION "message": { "$ref": "#/definitions/ThemeEnvironment/anyOf/1/properties/error/anyOf/0/properties/message" }, + "code": { + "$ref": "#/definitions/ThemeEnvironment/anyOf/1/properties/error/anyOf/0/properties/code" + }, "tryMessage": { "$ref": "#/definitions/ThemeEnvironment/anyOf/1/properties/error/anyOf/0/properties/tryMessage" }, diff --git a/packages/cli/oclif.manifest.json b/packages/cli/oclif.manifest.json index 6a1e866c5f0..52594142194 100644 --- a/packages/cli/oclif.manifest.json +++ b/packages/cli/oclif.manifest.json @@ -12331,7 +12331,7 @@ "args": { }, "customPluginName": "@shopify/theme", - "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\nUse `--json-schema` to print the result, error, and event schemas.\n\nOutput from `--json` conforms to the `ThemePublishResult` schema.\n\n```json\n{\n \"anyOf\": [\n {\n \"$ref\": \"#/definitions/ThemeEnvironment/anyOf/0/properties/result\"\n },\n {\n \"type\": \"object\",\n \"properties\": {\n \"environments\": {\n \"type\": \"array\",\n \"items\": {\n \"$ref\": \"#/definitions/ThemeEnvironment\"\n }\n }\n },\n \"required\": [\n \"environments\"\n ],\n \"additionalProperties\": false\n }\n ],\n \"title\": \"ThemePublishResult\",\n \"definitions\": {\n \"Theme\": {\n \"type\": \"object\",\n \"properties\": {\n \"id\": {\n \"type\": \"string\",\n \"pattern\": \"^\\\\d+$\",\n \"description\": \"The decimal Online Store theme ID, not a Shopify GID.\"\n },\n \"name\": {\n \"type\": \"string\"\n },\n \"role\": {\n \"type\": \"string\",\n \"description\": \"The upstream theme role; known values include live, unpublished, and development.\"\n },\n \"storeDomain\": {\n \"anyOf\": [\n {\n \"type\": \"string\",\n \"pattern\": \"^[a-z0-9][a-z0-9-]*\\\\.myshopify\\\\.com$\"\n },\n {\n \"type\": \"null\"\n }\n ]\n },\n \"processing\": {\n \"type\": \"boolean\"\n },\n \"sourceUrl\": {\n \"anyOf\": [\n {\n \"type\": \"string\",\n \"format\": \"uri\"\n },\n {\n \"type\": \"null\"\n }\n ]\n }\n },\n \"required\": [\n \"id\",\n \"name\",\n \"role\",\n \"storeDomain\",\n \"processing\",\n \"sourceUrl\"\n ],\n \"additionalProperties\": false,\n \"description\": \"The selected theme projection; sourceUrl is null when the upstream source URL is unavailable.\"\n },\n \"ThemeEnvironment\": {\n \"anyOf\": [\n {\n \"type\": \"object\",\n \"properties\": {\n \"environment\": {\n \"type\": \"string\"\n },\n \"result\": {\n \"anyOf\": [\n {\n \"type\": \"object\",\n \"properties\": {\n \"status\": {\n \"type\": \"string\",\n \"const\": \"success\"\n },\n \"changed\": {\n \"type\": \"boolean\"\n },\n \"theme\": {\n \"$ref\": \"#/definitions/Theme\"\n }\n },\n \"required\": [\n \"status\",\n \"changed\",\n \"theme\"\n ],\n \"additionalProperties\": false\n },\n {\n \"type\": \"object\",\n \"properties\": {\n \"status\": {\n \"type\": \"string\",\n \"const\": \"cancelled\"\n }\n },\n \"required\": [\n \"status\"\n ],\n \"additionalProperties\": false\n }\n ]\n }\n },\n \"required\": [\n \"environment\",\n \"result\"\n ],\n \"additionalProperties\": false\n },\n {\n \"type\": \"object\",\n \"properties\": {\n \"environment\": {\n \"type\": \"string\"\n },\n \"error\": {\n \"anyOf\": [\n {\n \"type\": \"object\",\n \"properties\": {\n \"type\": {\n \"type\": \"string\",\n \"const\": \"abort\"\n },\n \"message\": {\n \"type\": \"string\"\n },\n \"tryMessage\": {\n \"type\": \"string\"\n },\n \"nextSteps\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\"\n }\n },\n \"customSections\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"properties\": {\n \"title\": {\n \"type\": \"string\"\n },\n \"body\": {\n \"anyOf\": [\n {\n \"type\": \"string\"\n },\n {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\"\n }\n }\n }\n ]\n }\n },\n \"required\": [\n \"body\"\n ],\n \"additionalProperties\": false\n }\n },\n \"details\": {}\n },\n \"required\": [\n \"type\",\n \"message\"\n ],\n \"additionalProperties\": false\n },\n {\n \"type\": \"object\",\n \"properties\": {\n \"type\": {\n \"type\": \"string\",\n \"const\": \"bug\"\n },\n \"message\": {\n \"$ref\": \"#/definitions/ThemeEnvironment/anyOf/1/properties/error/anyOf/0/properties/message\"\n },\n \"tryMessage\": {\n \"$ref\": \"#/definitions/ThemeEnvironment/anyOf/1/properties/error/anyOf/0/properties/tryMessage\"\n },\n \"nextSteps\": {\n \"$ref\": \"#/definitions/ThemeEnvironment/anyOf/1/properties/error/anyOf/0/properties/nextSteps\"\n },\n \"customSections\": {\n \"$ref\": \"#/definitions/ThemeEnvironment/anyOf/1/properties/error/anyOf/0/properties/customSections\"\n },\n \"details\": {\n \"$ref\": \"#/definitions/ThemeEnvironment/anyOf/1/properties/error/anyOf/0/properties/details\"\n },\n \"stack\": {\n \"type\": \"string\"\n }\n },\n \"required\": [\n \"type\",\n \"message\"\n ],\n \"additionalProperties\": false\n },\n {\n \"type\": \"object\",\n \"properties\": {\n \"type\": {\n \"type\": \"string\",\n \"const\": \"external\"\n },\n \"message\": {\n \"$ref\": \"#/definitions/ThemeEnvironment/anyOf/1/properties/error/anyOf/0/properties/message\"\n },\n \"tryMessage\": {\n \"$ref\": \"#/definitions/ThemeEnvironment/anyOf/1/properties/error/anyOf/0/properties/tryMessage\"\n },\n \"nextSteps\": {\n \"$ref\": \"#/definitions/ThemeEnvironment/anyOf/1/properties/error/anyOf/0/properties/nextSteps\"\n },\n \"customSections\": {\n \"$ref\": \"#/definitions/ThemeEnvironment/anyOf/1/properties/error/anyOf/0/properties/customSections\"\n },\n \"details\": {\n \"$ref\": \"#/definitions/ThemeEnvironment/anyOf/1/properties/error/anyOf/0/properties/details\"\n },\n \"command\": {\n \"type\": \"string\"\n },\n \"args\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\"\n }\n }\n },\n \"required\": [\n \"type\",\n \"message\",\n \"command\",\n \"args\"\n ],\n \"additionalProperties\": false\n }\n ]\n }\n },\n \"required\": [\n \"environment\",\n \"error\"\n ],\n \"additionalProperties\": false\n }\n ]\n }\n },\n \"$schema\": \"http://json-schema.org/draft-07/schema#\"\n}\n```", + "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\nUse `--json-schema` to print the result, error, and event schemas.\n\nOutput from `--json` conforms to the `ThemePublishResult` schema.\n\n```json\n{\n \"anyOf\": [\n {\n \"$ref\": \"#/definitions/ThemeEnvironment/anyOf/0/properties/result\"\n },\n {\n \"type\": \"object\",\n \"properties\": {\n \"environments\": {\n \"type\": \"array\",\n \"items\": {\n \"$ref\": \"#/definitions/ThemeEnvironment\"\n }\n }\n },\n \"required\": [\n \"environments\"\n ],\n \"additionalProperties\": false\n }\n ],\n \"title\": \"ThemePublishResult\",\n \"definitions\": {\n \"Theme\": {\n \"type\": \"object\",\n \"properties\": {\n \"id\": {\n \"type\": \"string\",\n \"pattern\": \"^\\\\d+$\",\n \"description\": \"The decimal Online Store theme ID, not a Shopify GID.\"\n },\n \"name\": {\n \"type\": \"string\"\n },\n \"role\": {\n \"type\": \"string\",\n \"description\": \"The upstream theme role; known values include live, unpublished, and development.\"\n },\n \"storeDomain\": {\n \"anyOf\": [\n {\n \"type\": \"string\",\n \"pattern\": \"^[a-z0-9][a-z0-9-]*\\\\.myshopify\\\\.com$\"\n },\n {\n \"type\": \"null\"\n }\n ]\n },\n \"processing\": {\n \"type\": \"boolean\"\n },\n \"sourceUrl\": {\n \"anyOf\": [\n {\n \"type\": \"string\",\n \"format\": \"uri\"\n },\n {\n \"type\": \"null\"\n }\n ]\n }\n },\n \"required\": [\n \"id\",\n \"name\",\n \"role\",\n \"storeDomain\",\n \"processing\",\n \"sourceUrl\"\n ],\n \"additionalProperties\": false,\n \"description\": \"The selected theme projection; sourceUrl is null when the upstream source URL is unavailable.\"\n },\n \"ThemeEnvironment\": {\n \"anyOf\": [\n {\n \"type\": \"object\",\n \"properties\": {\n \"environment\": {\n \"type\": \"string\"\n },\n \"result\": {\n \"anyOf\": [\n {\n \"type\": \"object\",\n \"properties\": {\n \"status\": {\n \"type\": \"string\",\n \"const\": \"success\"\n },\n \"changed\": {\n \"type\": \"boolean\"\n },\n \"theme\": {\n \"$ref\": \"#/definitions/Theme\"\n }\n },\n \"required\": [\n \"status\",\n \"changed\",\n \"theme\"\n ],\n \"additionalProperties\": false\n },\n {\n \"type\": \"object\",\n \"properties\": {\n \"status\": {\n \"type\": \"string\",\n \"const\": \"cancelled\"\n }\n },\n \"required\": [\n \"status\"\n ],\n \"additionalProperties\": false\n }\n ]\n }\n },\n \"required\": [\n \"environment\",\n \"result\"\n ],\n \"additionalProperties\": false\n },\n {\n \"type\": \"object\",\n \"properties\": {\n \"environment\": {\n \"type\": \"string\"\n },\n \"error\": {\n \"anyOf\": [\n {\n \"type\": \"object\",\n \"properties\": {\n \"type\": {\n \"type\": \"string\",\n \"const\": \"abort\"\n },\n \"message\": {\n \"type\": \"string\"\n },\n \"code\": {\n \"type\": \"string\",\n \"minLength\": 1,\n \"description\": \"A stable error code, included only when known.\"\n },\n \"tryMessage\": {\n \"type\": \"string\"\n },\n \"nextSteps\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\"\n }\n },\n \"customSections\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"properties\": {\n \"title\": {\n \"type\": \"string\"\n },\n \"body\": {\n \"anyOf\": [\n {\n \"type\": \"string\"\n },\n {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\"\n }\n }\n }\n ]\n }\n },\n \"required\": [\n \"body\"\n ],\n \"additionalProperties\": false\n }\n },\n \"details\": {\n \"description\": \"Selected domain details, preserving native API payloads such as GraphQL errors, extensions, and data.\"\n }\n },\n \"required\": [\n \"type\",\n \"message\"\n ],\n \"additionalProperties\": false\n },\n {\n \"type\": \"object\",\n \"properties\": {\n \"type\": {\n \"type\": \"string\",\n \"const\": \"bug\"\n },\n \"message\": {\n \"$ref\": \"#/definitions/ThemeEnvironment/anyOf/1/properties/error/anyOf/0/properties/message\"\n },\n \"code\": {\n \"$ref\": \"#/definitions/ThemeEnvironment/anyOf/1/properties/error/anyOf/0/properties/code\"\n },\n \"tryMessage\": {\n \"$ref\": \"#/definitions/ThemeEnvironment/anyOf/1/properties/error/anyOf/0/properties/tryMessage\"\n },\n \"nextSteps\": {\n \"$ref\": \"#/definitions/ThemeEnvironment/anyOf/1/properties/error/anyOf/0/properties/nextSteps\"\n },\n \"customSections\": {\n \"$ref\": \"#/definitions/ThemeEnvironment/anyOf/1/properties/error/anyOf/0/properties/customSections\"\n },\n \"details\": {\n \"$ref\": \"#/definitions/ThemeEnvironment/anyOf/1/properties/error/anyOf/0/properties/details\"\n },\n \"stack\": {\n \"type\": \"string\"\n }\n },\n \"required\": [\n \"type\",\n \"message\"\n ],\n \"additionalProperties\": false\n },\n {\n \"type\": \"object\",\n \"properties\": {\n \"type\": {\n \"type\": \"string\",\n \"const\": \"external\"\n },\n \"message\": {\n \"$ref\": \"#/definitions/ThemeEnvironment/anyOf/1/properties/error/anyOf/0/properties/message\"\n },\n \"code\": {\n \"$ref\": \"#/definitions/ThemeEnvironment/anyOf/1/properties/error/anyOf/0/properties/code\"\n },\n \"tryMessage\": {\n \"$ref\": \"#/definitions/ThemeEnvironment/anyOf/1/properties/error/anyOf/0/properties/tryMessage\"\n },\n \"nextSteps\": {\n \"$ref\": \"#/definitions/ThemeEnvironment/anyOf/1/properties/error/anyOf/0/properties/nextSteps\"\n },\n \"customSections\": {\n \"$ref\": \"#/definitions/ThemeEnvironment/anyOf/1/properties/error/anyOf/0/properties/customSections\"\n },\n \"details\": {\n \"$ref\": \"#/definitions/ThemeEnvironment/anyOf/1/properties/error/anyOf/0/properties/details\"\n },\n \"command\": {\n \"type\": \"string\"\n },\n \"args\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\"\n }\n }\n },\n \"required\": [\n \"type\",\n \"message\",\n \"command\",\n \"args\"\n ],\n \"additionalProperties\": false\n }\n ]\n }\n },\n \"required\": [\n \"environment\",\n \"error\"\n ],\n \"additionalProperties\": false\n }\n ]\n }\n },\n \"$schema\": \"http://json-schema.org/draft-07/schema#\"\n}\n```", "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.", "enableJsonFlag": false, "flags": { diff --git a/packages/theme/src/cli/commands/theme/publish.test.ts b/packages/theme/src/cli/commands/theme/publish.test.ts index 51c27ce52ec..e97772239bf 100644 --- a/packages/theme/src/cli/commands/theme/publish.test.ts +++ b/packages/theme/src/cli/commands/theme/publish.test.ts @@ -79,6 +79,16 @@ describe('theme publish JSON output', () => { }, ) + test('returns changed false for an already-live theme', async () => { + vi.mocked(findOrSelectTheme).mockResolvedValue(publishedTheme) + await withCapturedStandardStreams(async ({stdout, stderr}) => { + await run(['--store', store, '--theme', '1', '--json', '--no-input', '--force']) + expect(JSON.parse(stdout())).toEqual({status: 'success', changed: false, theme: publicTheme}) + expect(stderr()).toBe('') + }) + expect(themePublish).not.toHaveBeenCalled() + }) + test('routes diagnostics to stderr', async () => { vi.mocked(findOrSelectTheme).mockResolvedValue(originalTheme) vi.mocked(themePublish).mockImplementation(async () => { @@ -127,7 +137,7 @@ describe('theme publish JSON output', () => { theme: {...publicTheme, storeDomain: `${environment}.myshopify.com`}, }, } - : {environment, error: {type: 'abort', message: 'Publishing failed'}}, + : {environment, error: {type: 'bug', message: 'Publishing failed', stack: expect.any(String)}}, ), }) if (failures === 'none') { @@ -147,12 +157,15 @@ describe('theme publish JSON output', () => { }) test.each([ - {status: 'success', theme: {...publishedTheme, id: '1', shop: store}}, - {status: 'success', theme: {...publishedTheme, shop: null}}, - {status: 'success', theme: {...publishedTheme, src: false, shop: store}}, + {status: 'success', changed: true, theme: {...publishedTheme, id: '1', shop: store}}, + {status: 'success', changed: true, theme: {...publishedTheme, shop: null}}, + {status: 'success', changed: true, theme: {...publishedTheme, src: false, shop: store}}, { environments: [ - {environment: 'first', result: {status: 'success', theme: {...publishedTheme, role: null, shop: store}}}, + { + environment: 'first', + result: {status: 'success', changed: true, theme: {...publishedTheme, role: null, shop: store}}, + }, ], }, ])('rejects malformed results %#', (result) => { diff --git a/packages/theme/src/cli/commands/theme/publish.ts b/packages/theme/src/cli/commands/theme/publish.ts index ca0689d8473..f6d02185bc1 100644 --- a/packages/theme/src/cli/commands/theme/publish.ts +++ b/packages/theme/src/cli/commands/theme/publish.ts @@ -60,10 +60,6 @@ If you want to publish your local theme, then you need to run \`shopify theme pu renderThemePublishResult(result, flags.json ? 'json' : 'text', flags.environment) } - protected collectsEnvironmentResults(flags: {json?: boolean}): boolean { - return Boolean(flags.json) - } - protected renderEnvironmentResults( environments: import('../../services/json-output/schema.js').ThemeEnvironmentResult[], ): void { diff --git a/packages/theme/src/cli/services/publish.test.ts b/packages/theme/src/cli/services/publish.test.ts index 305fe05f5ae..f603af6b497 100644 --- a/packages/theme/src/cli/services/publish.test.ts +++ b/packages/theme/src/cli/services/publish.test.ts @@ -27,6 +27,31 @@ const options = { } describe('publish', () => { + test('returns an unchanged result for a live theme without confirmation or publishing', async () => { + const liveTheme = {...theme, role: 'live'} + vi.mocked(findOrSelectTheme).mockResolvedValue(liveTheme) + + const result = await publish(session, options) + + expect(result).toEqual({ + data: {status: 'success', changed: false, theme: {...liveTheme, shop: session.storeFqdn}}, + originalTheme: liveTheme, + previewUrl: 'https://my-shop.myshopify.com', + }) + expect(renderConfirmationPrompt).not.toHaveBeenCalled() + expect(themePublish).not.toHaveBeenCalled() + expect(renderSuccess).toHaveBeenCalledWith({ + body: [ + 'The theme', + "'my theme'", + {subdued: '(#1)'}, + 'is already live at', + {link: {label: 'https://my-shop.myshopify.com', url: 'https://my-shop.myshopify.com'}}, + {char: '.'}, + ], + }) + }) + test('prompts for confirmation, publishes the theme and renders the theme link', async () => { // Given vi.mocked(findOrSelectTheme).mockResolvedValue(theme) @@ -119,7 +144,7 @@ test('returns the published API data without presenting a final result', async ( vi.mocked(themePublish).mockResolvedValue(publishedTheme) const result = await executePublish(session, {...options, force: true}) expect(result).toEqual({ - data: {status: 'success', theme: {...publishedTheme, shop: session.storeFqdn}}, + data: {status: 'success', changed: true, theme: {...publishedTheme, shop: session.storeFqdn}}, originalTheme: theme, previewUrl: 'https://my-shop.myshopify.com', }) diff --git a/packages/theme/src/cli/services/publish.ts b/packages/theme/src/cli/services/publish.ts index ba08560b471..dd3168a9bb6 100644 --- a/packages/theme/src/cli/services/publish.ts +++ b/packages/theme/src/cli/services/publish.ts @@ -27,6 +27,14 @@ export async function publish( const previewUrl = themePreviewUrl({...themeToPublish, role: 'live'} as Theme, adminSession) + if (themeToPublish.role === 'live') { + return { + data: {status: 'success', changed: false, theme: {...themeToPublish, shop: adminSession.storeFqdn}}, + originalTheme: themeToPublish, + previewUrl, + } + } + if (!options.force && !multiEnvironment) { const accept = await renderConfirmationPrompt({ message: `Do you want to make '${themeToPublish.name}' the new live theme on ${adminSession.storeFqdn}?`, @@ -39,7 +47,7 @@ export async function publish( // The API helper rejects missing themes and user errors before returning. const publishedTheme = (await themePublish(themeToPublish.id, adminSession))! return { - data: {status: 'success', theme: {...publishedTheme, shop: adminSession.storeFqdn}}, + data: {status: 'success', changed: true, theme: {...publishedTheme, shop: adminSession.storeFqdn}}, originalTheme: themeToPublish, previewUrl, } diff --git a/packages/theme/src/cli/services/publish/result.ts b/packages/theme/src/cli/services/publish/result.ts index 8ea2a9b2506..2166a55d9d4 100644 --- a/packages/theme/src/cli/services/publish/result.ts +++ b/packages/theme/src/cli/services/publish/result.ts @@ -17,7 +17,7 @@ export function renderThemePublishResult( body: [ 'The theme', ...themeComponent(result.originalTheme), - 'is now live at', + result.data.changed ? 'is now live at' : 'is already live at', {link: {label: result.previewUrl, url: result.previewUrl}}, {char: '.'}, ], diff --git a/packages/theme/src/cli/services/publish/types.ts b/packages/theme/src/cli/services/publish/types.ts index 1a259306ecd..b2a5b574e9d 100644 --- a/packages/theme/src/cli/services/publish/types.ts +++ b/packages/theme/src/cli/services/publish/types.ts @@ -9,6 +9,7 @@ import {zod} from '@shopify/cli-kit/node/schema' import type {Theme} from '@shopify/cli-kit/node/themes/types' const ThemePublishResultSchema = ThemeMutationSuccessSchema.extend({ + changed: zod.boolean(), theme: ThemeMutationThemeSchema, }) export const themePublishJsonOutputSchema = defineThemeJsonOutputSchema({ @@ -19,7 +20,7 @@ export const themePublishJsonOutputSchema = defineThemeJsonOutputSchema({ definitions: {Theme: ThemeMutationJsonThemeSchema}, project(value) { const result = ThemePublishResultSchema.parse(value) - return {status: result.status, changed: true, theme: projectThemeMutationTheme(result.theme)} + return {status: result.status, changed: result.changed, theme: projectThemeMutationTheme(result.theme)} }, })