Skip to content

Commit f43cea5

Browse files
authored
Merge pull request #6657 from Shopify/gg-push-with-name
Allow `--development` flag to create a new theme
2 parents 4892638 + 35ba22b commit f43cea5

20 files changed

Lines changed: 550 additions & 44 deletions

File tree

.changeset/every-years-study.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@shopify/cli-kit': patch
3+
---
4+
5+
Set the current theme in local development on create or find

.changeset/nice-clowns-bathe.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
'@shopify/cli-kit': patch
3+
'@shopify/theme': patch
4+
'@shopify/cli': patch
5+
---
6+
7+
Change wording for current development theme in `theme list`
8+
9+
Previously you could only have one development theme at a time so we'd add `[yours]` beside the development theme that you were currently attached to. Now you can have multiple development themes so we're changing the language to `[current]` to show which theme you are actively connected to.

.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
}
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
/* eslint-disable @typescript-eslint/consistent-type-definitions */
2+
import * as Types from './types.js'
3+
4+
import {TypedDocumentNode as DocumentNode} from '@graphql-typed-document-node/core'
5+
6+
export type FindDevelopmentThemeByNameQueryVariables = Types.Exact<{
7+
name: Types.Scalars['String']['input']
8+
}>
9+
10+
export type FindDevelopmentThemeByNameQuery = {
11+
themes?: {nodes: {id: string; name: string; role: Types.ThemeRole; processing: boolean}[]} | null
12+
}
13+
14+
export const FindDevelopmentThemeByName = {
15+
kind: 'Document',
16+
definitions: [
17+
{
18+
kind: 'OperationDefinition',
19+
operation: 'query',
20+
name: {kind: 'Name', value: 'findDevelopmentThemeByName'},
21+
variableDefinitions: [
22+
{
23+
kind: 'VariableDefinition',
24+
variable: {kind: 'Variable', name: {kind: 'Name', value: 'name'}},
25+
type: {kind: 'NonNullType', type: {kind: 'NamedType', name: {kind: 'Name', value: 'String'}}},
26+
},
27+
],
28+
selectionSet: {
29+
kind: 'SelectionSet',
30+
selections: [
31+
{
32+
kind: 'Field',
33+
name: {kind: 'Name', value: 'themes'},
34+
arguments: [
35+
{kind: 'Argument', name: {kind: 'Name', value: 'first'}, value: {kind: 'IntValue', value: '2'}},
36+
{
37+
kind: 'Argument',
38+
name: {kind: 'Name', value: 'names'},
39+
value: {kind: 'ListValue', values: [{kind: 'Variable', name: {kind: 'Name', value: 'name'}}]},
40+
},
41+
{
42+
kind: 'Argument',
43+
name: {kind: 'Name', value: 'roles'},
44+
value: {kind: 'ListValue', values: [{kind: 'EnumValue', value: 'DEVELOPMENT'}]},
45+
},
46+
],
47+
selectionSet: {
48+
kind: 'SelectionSet',
49+
selections: [
50+
{
51+
kind: 'Field',
52+
name: {kind: 'Name', value: 'nodes'},
53+
selectionSet: {
54+
kind: 'SelectionSet',
55+
selections: [
56+
{kind: 'Field', name: {kind: 'Name', value: 'id'}},
57+
{kind: 'Field', name: {kind: 'Name', value: 'name'}},
58+
{kind: 'Field', name: {kind: 'Name', value: 'role'}},
59+
{kind: 'Field', name: {kind: 'Name', value: 'processing'}},
60+
{kind: 'Field', name: {kind: 'Name', value: '__typename'}},
61+
],
62+
},
63+
},
64+
{kind: 'Field', name: {kind: 'Name', value: '__typename'}},
65+
],
66+
},
67+
},
68+
],
69+
},
70+
},
71+
],
72+
} as unknown as DocumentNode<FindDevelopmentThemeByNameQuery, FindDevelopmentThemeByNameQueryVariables>
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
query findDevelopmentThemeByName($name: String!) {
2+
themes(first: 2, names: [$name], roles: [DEVELOPMENT]) {
3+
nodes {
4+
id
5+
name
6+
role
7+
processing
8+
}
9+
}
10+
}

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

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
themeDuplicate,
55
fetchTheme,
66
fetchThemes,
7+
findDevelopmentThemeByName,
78
ThemeParams,
89
themeUpdate,
910
themePublish,
@@ -24,6 +25,7 @@ import {ThemeFilesUpsert} from '../../../cli/api/graphql/admin/generated/theme_f
2425
import {ThemeFilesDelete} from '../../../cli/api/graphql/admin/generated/theme_files_delete.js'
2526
import {GetThemes} from '../../../cli/api/graphql/admin/generated/get_themes.js'
2627
import {GetTheme} from '../../../cli/api/graphql/admin/generated/get_theme.js'
28+
import {FindDevelopmentThemeByName} from '../../../cli/api/graphql/admin/generated/find_development_theme_by_name.js'
2729
import {adminRequestDoc, supportedApiVersions} from '../api/admin.js'
2830
import {AbortError} from '../error.js'
2931

@@ -89,6 +91,68 @@ describe('fetchTheme', () => {
8991
})
9092
})
9193

94+
describe('findDevelopmentThemeByName', () => {
95+
test('returns a development theme with a specific name', async () => {
96+
vi.mocked(adminRequestDoc).mockResolvedValue({
97+
themes: {
98+
nodes: [{id: 'gid://shopify/OnlineStoreTheme/1', name: 'PR-123', role: 'DEVELOPMENT', processing: false}],
99+
},
100+
})
101+
102+
// When
103+
const theme = await findDevelopmentThemeByName('PR-123', session)
104+
105+
// Then
106+
expect(adminRequestDoc).toHaveBeenCalledWith({
107+
query: FindDevelopmentThemeByName,
108+
session,
109+
variables: {name: 'PR-123'},
110+
responseOptions: {handleErrors: false},
111+
preferredBehaviour: expectedApiOptions,
112+
})
113+
114+
expect(theme).not.toBeNull()
115+
expect(theme!.id).toEqual(1)
116+
expect(theme!.name).toEqual('PR-123')
117+
expect(theme!.processing).toBeFalsy()
118+
})
119+
120+
test('returns undefined when a theme is not found', async () => {
121+
vi.mocked(adminRequestDoc).mockResolvedValue({
122+
themes: {
123+
nodes: [],
124+
},
125+
})
126+
127+
const theme = await findDevelopmentThemeByName('PR-123', session)
128+
129+
expect(theme).toBeUndefined()
130+
})
131+
132+
test('aborts if there is more than one development theme with the same name', async () => {
133+
vi.mocked(adminRequestDoc).mockResolvedValue({
134+
themes: {
135+
nodes: [
136+
{id: 'gid://shopify/OnlineStoreTheme/1', name: 'PR-123', role: 'DEVELOPMENT', processing: true},
137+
{id: 'gid://shopify/OnlineStoreTheme/2', name: 'PR-123', role: 'DEVELOPMENT', processing: true},
138+
],
139+
},
140+
})
141+
142+
await expect(findDevelopmentThemeByName('PR-123', session)).rejects.toThrow(AbortError)
143+
})
144+
145+
test('returns undefined when the query errors', async () => {
146+
const errorResponse = {
147+
status: 200,
148+
errors: [{message: 'Theme not found'} as any],
149+
}
150+
vi.mocked(adminRequestDoc).mockRejectedValue(new ClientError(errorResponse, {query: ''}))
151+
152+
await expect(findDevelopmentThemeByName('PR-123', session)).rejects.toThrow()
153+
})
154+
})
155+
92156
describe('fetchThemes', () => {
93157
test('returns store themes', async () => {
94158
// Given

packages/cli-kit/src/public/node/themes/api.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import {
2222
import {MetafieldDefinitionsByOwnerType} from '../../../cli/api/graphql/admin/generated/metafield_definitions_by_owner_type.js'
2323
import {GetThemes} from '../../../cli/api/graphql/admin/generated/get_themes.js'
2424
import {GetTheme} from '../../../cli/api/graphql/admin/generated/get_theme.js'
25+
import {FindDevelopmentThemeByName} from '../../../cli/api/graphql/admin/generated/find_development_theme_by_name.js'
2526
import {OnlineStorePasswordProtection} from '../../../cli/api/graphql/admin/generated/online_store_password_protection.js'
2627
import {RequestModeInput} from '../http.js'
2728
import {adminRequestDoc} from '../api/admin.js'
@@ -113,6 +114,38 @@ export async function fetchThemes(session: AdminSession): Promise<Theme[]> {
113114
}
114115
}
115116

117+
export async function findDevelopmentThemeByName(name: string, session: AdminSession): Promise<Theme | undefined> {
118+
recordEvent('theme-api:find-development-theme-by-name')
119+
120+
const {themes} = await adminRequestDoc({
121+
query: FindDevelopmentThemeByName,
122+
session,
123+
variables: {name},
124+
responseOptions: {handleErrors: false},
125+
preferredBehaviour: THEME_API_NETWORK_BEHAVIOUR,
126+
})
127+
128+
if (!themes) {
129+
unexpectedGraphQLError('Failed to fetch themes')
130+
}
131+
132+
if (themes.nodes.length > 1) {
133+
throw recordError(new AbortError(`More than one development theme is named "${name}"`))
134+
}
135+
136+
if (themes.nodes.length === 1) {
137+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
138+
const {id, processing, role, name} = themes.nodes[0]!
139+
140+
return buildTheme({
141+
id: parseGid(id),
142+
processing,
143+
role: role.toLowerCase(),
144+
name,
145+
})
146+
}
147+
}
148+
116149
export async function themeCreate(params: ThemeParams, session: AdminSession): Promise<Theme | undefined> {
117150
const themeSource = params.src ?? SkeletonThemeCdn
118151
recordEvent('theme-api:create-theme')

0 commit comments

Comments
 (0)