Skip to content

Commit 825b91e

Browse files
committed
Add ability to set theme name on push
1 parent 7abf580 commit 825b91e

10 files changed

Lines changed: 128 additions & 32 deletions

File tree

.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+
This gives developers the ability to programatically create new 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
@@ -6938,6 +6938,15 @@
69386938
"isOptional": true,
69396939
"environmentValue": "SHOPIFY_FLAG_ALLOW_LIVE"
69406940
},
6941+
{
6942+
"filePath": "docs-shopify.dev/commands/interfaces/theme-push.interface.ts",
6943+
"syntaxKind": "PropertySignature",
6944+
"name": "-c, --development-context <value>",
6945+
"value": "string",
6946+
"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.",
6947+
"isOptional": true,
6948+
"environmentValue": "SHOPIFY_FLAG_DEVELOPMENT_CONTEXT"
6949+
},
69416950
{
69426951
"filePath": "docs-shopify.dev/commands/interfaces/theme-push.interface.ts",
69436952
"syntaxKind": "PropertySignature",
@@ -7038,7 +7047,7 @@
70387047
"environmentValue": "SHOPIFY_FLAG_IGNORE"
70397048
}
70407049
],
7041-
"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}"
7050+
"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}"
70427051
}
70437052
}
70447053
}

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

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,28 @@ describe('ThemeManager', () => {
9292
expect(result).toEqual(mockTheme)
9393
expect(manager.getStoredThemeId()).toBe('123')
9494
})
95+
96+
test('always creates a new theme when name is provided', async () => {
97+
// Given
98+
manager.setThemeId('123')
99+
const customTheme = {...mockTheme, name: 'Custom name'}
100+
vi.mocked(themeCreate).mockResolvedValue(customTheme)
101+
102+
// When
103+
const result = await manager.findOrCreate('Custom name')
104+
105+
// Then
106+
expect(fetchTheme).not.toHaveBeenCalled()
107+
expect(themeCreate).toHaveBeenCalledWith(
108+
{
109+
name: 'Custom name',
110+
role: DEVELOPMENT_THEME_ROLE,
111+
},
112+
session,
113+
)
114+
expect(result).toEqual(customTheme)
115+
expect(manager.getStoredThemeId()).toBe('123')
116+
})
95117
})
96118

97119
describe('fetch', () => {

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

Lines changed: 12 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,24 @@ 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): Promise<Theme> {
17+
let theme = await this.fetch(name)
1818
if (!theme) {
19-
theme = await this.create()
19+
theme = await this.create(DEVELOPMENT_THEME_ROLE, name)
2020
}
2121
return theme
2222
}
2323

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

packages/cli/README.md

Lines changed: 26 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -2383,28 +2383,32 @@ USAGE
23832383
$ shopify theme push --unpublished --json
23842384

23852385
FLAGS
2386-
-a, --allow-live Allow push to a live theme.
2387-
-d, --development Push theme files from your remote development theme.
2388-
-e, --environment=<value>... The environment to apply to the current command.
2389-
-j, --json Output the result as JSON.
2390-
-l, --live Push theme files from your remote live theme.
2391-
-n, --nodelete Prevent deleting remote files that don't exist locally.
2392-
-o, --only=<value>... Upload only the specified files (Multiple flags allowed). Wrap the value in double
2393-
quotes if you're using wildcards.
2394-
-p, --publish Publish as the live theme after uploading.
2395-
-s, --store=<value> Store URL. It can be the store prefix (example) or the full myshopify.com URL
2396-
(example.myshopify.com, https://example.myshopify.com).
2397-
-t, --theme=<value> Theme ID or name of the remote theme.
2398-
-u, --unpublished Create a new unpublished theme and push to it.
2399-
-x, --ignore=<value>... Skip uploading the specified files (Multiple flags allowed). Wrap the value in double
2400-
quotes if you're using wildcards.
2401-
--listing=<value> The listing preset to use for multi-preset themes. Applies preset files from
2402-
listings/[preset-name] directory.
2403-
--no-color Disable color output.
2404-
--password=<value> Password generated from the Theme Access app or an Admin API token.
2405-
--path=<value> The path where you want to run the command. Defaults to the current working directory.
2406-
--strict Require theme check to pass without errors before pushing. Warnings are allowed.
2407-
--verbose Increase the verbosity of the output.
2386+
-a, --allow-live Allow push to a live theme.
2387+
-c, --development-context=<value> Unique identifier for a development theme context (e.g., PR number, branch name).
2388+
Reuses an existing development theme with this context name, or creates one if none
2389+
exists.
2390+
-d, --development Push theme files from your remote development theme.
2391+
-e, --environment=<value>... The environment to apply to the current command.
2392+
-j, --json Output the result as JSON.
2393+
-l, --live Push theme files from your remote live theme.
2394+
-n, --nodelete Prevent deleting remote files that don't exist locally.
2395+
-o, --only=<value>... Upload only the specified files (Multiple flags allowed). Wrap the value in double
2396+
quotes if you're using wildcards.
2397+
-p, --publish Publish as the live theme after uploading.
2398+
-s, --store=<value> Store URL. It can be the store prefix (example) or the full myshopify.com URL
2399+
(example.myshopify.com, https://example.myshopify.com).
2400+
-t, --theme=<value> Theme ID or name of the remote theme.
2401+
-u, --unpublished Create a new unpublished theme and push to it.
2402+
-x, --ignore=<value>... Skip uploading the specified files (Multiple flags allowed). Wrap the value in
2403+
double quotes if you're using wildcards.
2404+
--listing=<value> The listing preset to use for multi-preset themes. Applies preset files from
2405+
listings/[preset-name] directory.
2406+
--no-color Disable color output.
2407+
--password=<value> Password generated from the Theme Access app or an Admin API token.
2408+
--path=<value> The path where you want to run the command. Defaults to the current working
2409+
directory.
2410+
--strict Require theme check to pass without errors before pushing. Warnings are allowed.
2411+
--verbose Increase the verbosity of the output.
24082412

24092413
DESCRIPTION
24102414
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
@@ -7082,6 +7082,21 @@
70827082
"name": "development",
70837083
"type": "boolean"
70847084
},
7085+
"development-context": {
7086+
"char": "c",
7087+
"dependsOn": [
7088+
"development"
7089+
],
7090+
"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.",
7091+
"env": "SHOPIFY_FLAG_DEVELOPMENT_CONTEXT",
7092+
"exclusive": [
7093+
"theme"
7094+
],
7095+
"hasDynamicHelp": false,
7096+
"multiple": false,
7097+
"name": "development-context",
7098+
"type": "option"
7099+
},
70857100
"environment": {
70867101
"char": "e",
70877102
"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
@@ -102,6 +102,14 @@ export default class Push extends ThemeCommand {
102102
'The listing preset to use for multi-preset themes. Applies preset files from listings/[preset-name] directory.',
103103
env: 'SHOPIFY_FLAG_LISTING',
104104
}),
105+
'development-context': Flags.string({
106+
char: 'c',
107+
description:
108+
'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.',
109+
env: 'SHOPIFY_FLAG_DEVELOPMENT_CONTEXT',
110+
dependsOn: ['development'],
111+
exclusive: ['theme'],
112+
}),
105113
environment: themeFlags.environment,
106114
}
107115

@@ -119,6 +127,7 @@ export default class Push extends ThemeCommand {
119127
{
120128
...flags,
121129
allowLive: flags['allow-live'],
130+
developmentContext: flags['development-context'],
122131
noColor: flags['no-color'],
123132
},
124133
adminSession,

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}))

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

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,9 @@ export interface PushFlags {
113113

114114
/** The listing preset to use for multi-preset themes. */
115115
listing?: string
116+
117+
/** Unique identifier for a development theme context. Reuses an existing development theme with this context name, or creates one if none exists. */
118+
developmentContext?: string
116119
}
117120

118121
/**
@@ -379,11 +382,11 @@ export async function createOrSelectTheme(
379382
flags: PushFlags,
380383
multiEnvironment?: boolean,
381384
): Promise<Theme | undefined> {
382-
const {live, development, unpublished, theme, environment} = flags
385+
const {live, development, unpublished, theme, environment, developmentContext} = flags
383386

384387
if (development) {
385388
const themeManager = new DevelopmentThemeManager(session)
386-
return themeManager.findOrCreate()
389+
return themeManager.findOrCreate(developmentContext)
387390
} else if (unpublished) {
388391
const themeName = theme ?? (await promptThemeName('Name of the new theme'))
389392
return themeCreate(

0 commit comments

Comments
 (0)