Skip to content

Commit 9a39b44

Browse files
committed
Add the ability to create new development themes
Adds a new flag called --development-context on theme push that will allow developers to manually generate new development themes. Particularly helpful in CI environments.
1 parent 34e19bc commit 9a39b44

File tree

10 files changed

+149
-38
lines changed

10 files changed

+149
-38
lines changed

.changeset/puny-plants-allow.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
'@shopify/cli-kit': minor
3+
'@shopify/theme': minor
4+
'@shopify/cli': minor
5+
---
6+
7+
Add `--development-context` flag to `theme push`
8+
9+
The new `--development-context` flag (short: `-c`) allows you to specify a unique identifier for a development theme context (e.g., PR number, branch name). This gives developers the ability to programmatically create or reuse named development themes; particularly useful when running `shopify theme push` in a CI environment where you might want to associate a particular development theme to a branch or pull request.

docs-shopify.dev/commands/interfaces/theme-push.interface.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,12 @@ export interface themepush {
1212
*/
1313
'-d, --development'?: ''
1414

15+
/**
16+
* Unique identifier for a development theme context (e.g., PR number, branch name). Reuses an existing development theme with this context name, or creates one if none exists.
17+
* @environment SHOPIFY_FLAG_DEVELOPMENT_CONTEXT
18+
*/
19+
'-c, --development-context <value>'?: string
20+
1521
/**
1622
* The environment to apply to the current command.
1723
* @environment SHOPIFY_FLAG_ENVIRONMENT

docs-shopify.dev/generated/generated_docs_data.json

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7541,6 +7541,15 @@
75417541
"isOptional": true,
75427542
"environmentValue": "SHOPIFY_FLAG_ALLOW_LIVE"
75437543
},
7544+
{
7545+
"filePath": "docs-shopify.dev/commands/interfaces/theme-push.interface.ts",
7546+
"syntaxKind": "PropertySignature",
7547+
"name": "-c, --development-context <value>",
7548+
"value": "string",
7549+
"description": "Unique identifier for a development theme context (e.g., PR number, branch name). Reuses an existing development theme with this context name, or creates one if none exists.",
7550+
"isOptional": true,
7551+
"environmentValue": "SHOPIFY_FLAG_DEVELOPMENT_CONTEXT"
7552+
},
75447553
{
75457554
"filePath": "docs-shopify.dev/commands/interfaces/theme-push.interface.ts",
75467555
"syntaxKind": "PropertySignature",
@@ -7641,7 +7650,7 @@
76417650
"environmentValue": "SHOPIFY_FLAG_IGNORE"
76427651
}
76437652
],
7644-
"value": "export interface themepush {\n /**\n * Allow push to a live theme.\n * @environment SHOPIFY_FLAG_ALLOW_LIVE\n */\n '-a, --allow-live'?: ''\n\n /**\n * Push theme files from your remote development theme.\n * @environment SHOPIFY_FLAG_DEVELOPMENT\n */\n '-d, --development'?: ''\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 uploading the specified files (Multiple flags allowed). Wrap the value in double quotes if you're using wildcards.\n * @environment SHOPIFY_FLAG_IGNORE\n */\n '-x, --ignore <value>'?: string\n\n /**\n * Output the result as JSON.\n * @environment SHOPIFY_FLAG_JSON\n */\n '-j, --json'?: ''\n\n /**\n * The listing preset to use for multi-preset themes. Applies preset files from listings/[preset-name] directory.\n * @environment SHOPIFY_FLAG_LISTING\n */\n '--listing <value>'?: string\n\n /**\n * Push theme files from your remote live theme.\n * @environment SHOPIFY_FLAG_LIVE\n */\n '-l, --live'?: ''\n\n /**\n * Disable color output.\n * @environment SHOPIFY_FLAG_NO_COLOR\n */\n '--no-color'?: ''\n\n /**\n * Prevent deleting remote files that don't exist locally.\n * @environment SHOPIFY_FLAG_NODELETE\n */\n '-n, --nodelete'?: ''\n\n /**\n * Upload only the specified files (Multiple flags allowed). Wrap the value in double quotes if you're using wildcards.\n * @environment SHOPIFY_FLAG_ONLY\n */\n '-o, --only <value>'?: string\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 * Publish as the live theme after uploading.\n * @environment SHOPIFY_FLAG_PUBLISH\n */\n '-p, --publish'?: ''\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 * Require theme check to pass without errors before pushing. Warnings are allowed.\n * @environment SHOPIFY_FLAG_STRICT_PUSH\n */\n '--strict'?: ''\n\n /**\n * Theme ID or name of the remote theme.\n * @environment SHOPIFY_FLAG_THEME_ID\n */\n '-t, --theme <value>'?: string\n\n /**\n * Create a new unpublished theme and push to it.\n * @environment SHOPIFY_FLAG_UNPUBLISHED\n */\n '-u, --unpublished'?: ''\n\n /**\n * Increase the verbosity of the output.\n * @environment SHOPIFY_FLAG_VERBOSE\n */\n '--verbose'?: ''\n}"
7653+
"value": "export interface themepush {\n /**\n * Allow push to a live theme.\n * @environment SHOPIFY_FLAG_ALLOW_LIVE\n */\n '-a, --allow-live'?: ''\n\n /**\n * Push theme files from your remote development theme.\n * @environment SHOPIFY_FLAG_DEVELOPMENT\n */\n '-d, --development'?: ''\n\n /**\n * Unique identifier for a development theme context (e.g., PR number, branch name). Reuses an existing development theme with this context name, or creates one if none exists.\n * @environment SHOPIFY_FLAG_DEVELOPMENT_CONTEXT\n */\n '-c, --development-context <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 uploading the specified files (Multiple flags allowed). Wrap the value in double quotes if you're using wildcards.\n * @environment SHOPIFY_FLAG_IGNORE\n */\n '-x, --ignore <value>'?: string\n\n /**\n * Output the result as JSON.\n * @environment SHOPIFY_FLAG_JSON\n */\n '-j, --json'?: ''\n\n /**\n * The listing preset to use for multi-preset themes. Applies preset files from listings/[preset-name] directory.\n * @environment SHOPIFY_FLAG_LISTING\n */\n '--listing <value>'?: string\n\n /**\n * Push theme files from your remote live theme.\n * @environment SHOPIFY_FLAG_LIVE\n */\n '-l, --live'?: ''\n\n /**\n * Disable color output.\n * @environment SHOPIFY_FLAG_NO_COLOR\n */\n '--no-color'?: ''\n\n /**\n * Prevent deleting remote files that don't exist locally.\n * @environment SHOPIFY_FLAG_NODELETE\n */\n '-n, --nodelete'?: ''\n\n /**\n * Upload only the specified files (Multiple flags allowed). Wrap the value in double quotes if you're using wildcards.\n * @environment SHOPIFY_FLAG_ONLY\n */\n '-o, --only <value>'?: string\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 * Publish as the live theme after uploading.\n * @environment SHOPIFY_FLAG_PUBLISH\n */\n '-p, --publish'?: ''\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 * Require theme check to pass without errors before pushing. Warnings are allowed.\n * @environment SHOPIFY_FLAG_STRICT_PUSH\n */\n '--strict'?: ''\n\n /**\n * Theme ID or name of the remote theme.\n * @environment SHOPIFY_FLAG_THEME_ID\n */\n '-t, --theme <value>'?: string\n\n /**\n * Create a new unpublished theme and push to it.\n * @environment SHOPIFY_FLAG_UNPUBLISHED\n */\n '-u, --unpublished'?: ''\n\n /**\n * Increase the verbosity of the output.\n * @environment SHOPIFY_FLAG_VERBOSE\n */\n '--verbose'?: ''\n}"
76457654
}
76467655
}
76477656
}

packages/cli-kit/src/public/node/themes/theme-manager.test.ts

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import {ThemeManager} from './theme-manager.js'
22
import {Theme} from './types.js'
3-
import {fetchTheme, themeCreate} from './api.js'
3+
import {fetchTheme, findDevelopmentThemeByName, themeCreate} from './api.js'
44
import {DEVELOPMENT_THEME_ROLE, UNPUBLISHED_THEME_ROLE} from './utils.js'
55
import {BugError} from '../error.js'
66
import {test, describe, expect, vi, beforeEach} from 'vitest'
@@ -92,10 +92,24 @@ describe('ThemeManager', () => {
9292
expect(result).toEqual(mockTheme)
9393
expect(manager.getStoredThemeId()).toBe('123')
9494
})
95+
96+
test('searches through development themes of a given name', async () => {
97+
// Given
98+
vi.mocked(findDevelopmentThemeByName).mockResolvedValue(mockTheme)
99+
100+
// When
101+
const result = await manager.findOrCreate('Dev', DEVELOPMENT_THEME_ROLE)
102+
103+
// Then
104+
expect(fetchTheme).not.toHaveBeenCalled()
105+
expect(findDevelopmentThemeByName).toHaveBeenCalledWith('Dev', session)
106+
expect(result).toEqual(mockTheme)
107+
expect(themeCreate).not.toHaveBeenCalled()
108+
})
95109
})
96110

97111
describe('fetch', () => {
98-
test('returns undefined when no themeId is set', async () => {
112+
test('returns undefined when no themeId or name is set', async () => {
99113
// Given
100114
manager.setThemeId(undefined)
101115

@@ -120,6 +134,19 @@ describe('ThemeManager', () => {
120134
expect(result).toEqual(mockTheme)
121135
})
122136

137+
test('fetches and returns a theme when name is set and role is development', async () => {
138+
// Given
139+
vi.mocked(findDevelopmentThemeByName).mockResolvedValue(mockTheme)
140+
141+
// When
142+
const result = await manager.fetch('Dev', DEVELOPMENT_THEME_ROLE)
143+
144+
// Then
145+
expect(fetchTheme).not.toHaveBeenCalled()
146+
expect(findDevelopmentThemeByName).toHaveBeenCalledWith('Dev', session)
147+
expect(result).toEqual(mockTheme)
148+
})
149+
123150
test('removes theme when fetch returns undefined', async () => {
124151
// Given
125152
manager.setThemeId('123')

packages/cli-kit/src/public/node/themes/theme-manager.ts

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import {fetchTheme, themeCreate} from './api.js'
1+
import {fetchTheme, findDevelopmentThemeByName, themeCreate} from './api.js'
22
import {Theme} from './types.js'
33
import {DEVELOPMENT_THEME_ROLE, Role} from './utils.js'
44
import {generateThemeName} from '../../../private/node/themes/generate-theme-name.js'
@@ -13,19 +13,25 @@ export abstract class ThemeManager {
1313

1414
constructor(protected adminSession: AdminSession) {}
1515

16-
async findOrCreate(): Promise<Theme> {
17-
let theme = await this.fetch()
16+
async findOrCreate(name?: string, role?: Role): Promise<Theme> {
17+
let theme = await this.fetch(name, role)
1818
if (!theme) {
19-
theme = await this.create()
19+
theme = await this.create(role, name)
2020
}
2121
return theme
2222
}
2323

24-
async fetch() {
25-
if (!this.themeId) {
24+
async fetch(name?: string, role?: Role) {
25+
if (!this.themeId && !name) {
2626
return
2727
}
28-
const theme = await fetchTheme(parseInt(this.themeId, 10), this.adminSession)
28+
29+
const theme =
30+
name && role === DEVELOPMENT_THEME_ROLE
31+
? await findDevelopmentThemeByName(name, this.adminSession)
32+
: // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
33+
await fetchTheme(parseInt(this.themeId!, 10), this.adminSession)
34+
2935
if (!theme) {
3036
this.removeTheme()
3137
}

packages/cli/README.md

Lines changed: 30 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -2635,31 +2635,36 @@ USAGE
26352635
$ shopify theme push --unpublished --json
26362636

26372637
FLAGS
2638-
-a, --allow-live [env: SHOPIFY_FLAG_ALLOW_LIVE] Allow push to a live theme.
2639-
-d, --development [env: SHOPIFY_FLAG_DEVELOPMENT] Push theme files from your remote development theme.
2640-
-e, --environment=<value>... [env: SHOPIFY_FLAG_ENVIRONMENT] The environment to apply to the current command.
2641-
-j, --json [env: SHOPIFY_FLAG_JSON] Output the result as JSON.
2642-
-l, --live [env: SHOPIFY_FLAG_LIVE] Push theme files from your remote live theme.
2643-
-n, --nodelete [env: SHOPIFY_FLAG_NODELETE] Prevent deleting remote files that don't exist locally.
2644-
-o, --only=<value>... [env: SHOPIFY_FLAG_ONLY] Upload only the specified files (Multiple flags allowed). Wrap
2645-
the value in double quotes if you're using wildcards.
2646-
-p, --publish [env: SHOPIFY_FLAG_PUBLISH] Publish as the live theme after uploading.
2647-
-s, --store=<value> [env: SHOPIFY_FLAG_STORE] Store URL. It can be the store prefix (example) or the full
2648-
myshopify.com URL (example.myshopify.com, https://example.myshopify.com).
2649-
-t, --theme=<value> [env: SHOPIFY_FLAG_THEME_ID] Theme ID or name of the remote theme.
2650-
-u, --unpublished [env: SHOPIFY_FLAG_UNPUBLISHED] Create a new unpublished theme and push to it.
2651-
-x, --ignore=<value>... [env: SHOPIFY_FLAG_IGNORE] Skip uploading the specified files (Multiple flags allowed).
2652-
Wrap the value in double quotes if you're using wildcards.
2653-
--listing=<value> [env: SHOPIFY_FLAG_LISTING] The listing preset to use for multi-preset themes. Applies
2654-
preset files from listings/[preset-name] directory.
2655-
--no-color [env: SHOPIFY_FLAG_NO_COLOR] Disable color output.
2656-
--password=<value> [env: SHOPIFY_CLI_THEME_TOKEN] Password generated from the Theme Access app or an Admin
2657-
API token.
2658-
--path=<value> [env: SHOPIFY_FLAG_PATH] The path where you want to run the command. Defaults to the
2659-
current working directory.
2660-
--strict [env: SHOPIFY_FLAG_STRICT_PUSH] Require theme check to pass without errors before
2661-
pushing. Warnings are allowed.
2662-
--verbose [env: SHOPIFY_FLAG_VERBOSE] Increase the verbosity of the output.
2638+
-a, --allow-live [env: SHOPIFY_FLAG_ALLOW_LIVE] Allow push to a live theme.
2639+
-c, --development-context=<value> [env: SHOPIFY_FLAG_DEVELOPMENT_CONTEXT] Unique identifier for a development theme
2640+
context (e.g., PR number, branch name). Reuses an existing development theme with
2641+
this context name, or creates one if none exists.
2642+
-d, --development [env: SHOPIFY_FLAG_DEVELOPMENT] Push theme files from your remote development
2643+
theme.
2644+
-e, --environment=<value>... [env: SHOPIFY_FLAG_ENVIRONMENT] The environment to apply to the current command.
2645+
-j, --json [env: SHOPIFY_FLAG_JSON] Output the result as JSON.
2646+
-l, --live [env: SHOPIFY_FLAG_LIVE] Push theme files from your remote live theme.
2647+
-n, --nodelete [env: SHOPIFY_FLAG_NODELETE] Prevent deleting remote files that don't exist
2648+
locally.
2649+
-o, --only=<value>... [env: SHOPIFY_FLAG_ONLY] Upload only the specified files (Multiple flags allowed).
2650+
Wrap the value in double quotes if you're using wildcards.
2651+
-p, --publish [env: SHOPIFY_FLAG_PUBLISH] Publish as the live theme after uploading.
2652+
-s, --store=<value> [env: SHOPIFY_FLAG_STORE] Store URL. It can be the store prefix (example) or the
2653+
full myshopify.com URL (example.myshopify.com, https://example.myshopify.com).
2654+
-t, --theme=<value> [env: SHOPIFY_FLAG_THEME_ID] Theme ID or name of the remote theme.
2655+
-u, --unpublished [env: SHOPIFY_FLAG_UNPUBLISHED] Create a new unpublished theme and push to it.
2656+
-x, --ignore=<value>... [env: SHOPIFY_FLAG_IGNORE] Skip uploading the specified files (Multiple flags
2657+
allowed). Wrap the value in double quotes if you're using wildcards.
2658+
--listing=<value> [env: SHOPIFY_FLAG_LISTING] The listing preset to use for multi-preset themes.
2659+
Applies preset files from listings/[preset-name] directory.
2660+
--no-color [env: SHOPIFY_FLAG_NO_COLOR] Disable color output.
2661+
--password=<value> [env: SHOPIFY_CLI_THEME_TOKEN] Password generated from the Theme Access app or an
2662+
Admin API token.
2663+
--path=<value> [env: SHOPIFY_FLAG_PATH] The path where you want to run the command. Defaults to
2664+
the current working directory.
2665+
--strict [env: SHOPIFY_FLAG_STRICT_PUSH] Require theme check to pass without errors before
2666+
pushing. Warnings are allowed.
2667+
--verbose [env: SHOPIFY_FLAG_VERBOSE] Increase the verbosity of the output.
26632668

26642669
DESCRIPTION
26652670
Uploads your local theme files to the connected store, overwriting the remote version if specified.

packages/cli/oclif.manifest.json

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7227,6 +7227,21 @@
72277227
"name": "development",
72287228
"type": "boolean"
72297229
},
7230+
"development-context": {
7231+
"char": "c",
7232+
"dependsOn": [
7233+
"development"
7234+
],
7235+
"description": "Unique identifier for a development theme context (e.g., PR number, branch name). Reuses an existing development theme with this context name, or creates one if none exists.",
7236+
"env": "SHOPIFY_FLAG_DEVELOPMENT_CONTEXT",
7237+
"exclusive": [
7238+
"theme"
7239+
],
7240+
"hasDynamicHelp": false,
7241+
"multiple": false,
7242+
"name": "development-context",
7243+
"type": "option"
7244+
},
72307245
"environment": {
72317246
"char": "e",
72327247
"description": "The environment to apply to the current command.",

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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,14 @@ export default class Push extends ThemeCommand {
6363
description: 'Push theme files from your remote development theme.',
6464
env: 'SHOPIFY_FLAG_DEVELOPMENT',
6565
}),
66+
'development-context': Flags.string({
67+
char: 'c',
68+
description:
69+
'Unique identifier for a development theme context (e.g., PR number, branch name). Reuses an existing development theme with this context name, or creates one if none exists.',
70+
env: 'SHOPIFY_FLAG_DEVELOPMENT_CONTEXT',
71+
dependsOn: ['development'],
72+
exclusive: ['theme'],
73+
}),
6674
live: Flags.boolean({
6775
char: 'l',
6876
description: 'Push theme files from your remote live theme.',
@@ -121,6 +129,7 @@ export default class Push extends ThemeCommand {
121129
...flags,
122130
allowLive: flags['allow-live'],
123131
noColor: flags['no-color'],
132+
developmentContext: flags['development-context'],
124133
},
125134
adminSession,
126135
multiEnvironment,

packages/theme/src/cli/services/push.test.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -292,6 +292,20 @@ describe('createOrSelectTheme', async () => {
292292
expect(setDevelopmentTheme).toHaveBeenCalled()
293293
})
294294

295+
test('creates development theme when development and development-context flags are provided', async () => {
296+
// Given
297+
vi.mocked(themeCreate).mockResolvedValue(buildTheme({id: 1, name: 'Custom name', role: DEVELOPMENT_THEME_ROLE}))
298+
vi.mocked(fetchTheme).mockResolvedValue(undefined)
299+
const flags: PushFlags = {development: true, developmentContext: 'Custom name'}
300+
301+
// When
302+
const theme = await createOrSelectTheme(adminSession, flags)
303+
304+
// Then
305+
expect(theme).toMatchObject({role: DEVELOPMENT_THEME_ROLE, name: 'Custom name'})
306+
expect(setDevelopmentTheme).toHaveBeenCalled()
307+
})
308+
295309
test('creates development theme when development and unpublished flags are provided', async () => {
296310
// Given
297311
vi.mocked(themeCreate).mockResolvedValue(buildTheme({id: 1, name: 'Theme', role: DEVELOPMENT_THEME_ROLE}))

0 commit comments

Comments
 (0)