Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .changeset/nice-clowns-bathe.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
'@shopify/cli-kit': patch
'@shopify/theme': patch
'@shopify/cli': patch
---

Change wording for current development theme in `theme list`

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.
13 changes: 13 additions & 0 deletions .changeset/puny-plants-allow.md
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👋🏻 @binarymonkey84! Going to respond here so we can thread replies

Maybe update the relevant docs to suggest commit ID?

This is fully up to theme developers to decide how they want to leverage this. Maybe we could add it as an example of configuring a GitHub action, though!

Can this be added to the tests?

This is already working! The additional functionality I've added doesn't have any effect on the --json flag so this should continue working as normal.

It would be good to add clarity on how long the dev context theme will live for (before it is auto-deleted by Shopify) when it is used in this way. [...] Can the docs be updated to communicate how and when these types of dev themes get deleted / how long they persist for and any rules around that?

I'm not sure if we've ever shared the exact timing for this. I'll sync with the team to see if we can get some official documentation around this. Keep in mind, too, that there is a limit to the amount of development themes you can have (also undocumented, we'll see about having official guidelines on that, too).

At the moment, the docs say dev themes are deleted when running 'shopify auth logout'.

This might have been true at some point but is not true now. We'll get this cleaned up.

How will this work in a CI context, when we'll be using a theme access password?

In CI you'll be running the same command but also passing the --password flag. Perhaps I'm misunderstanding? Or is this in relation to the confusing wording about when development themes are deleted?

Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
'@shopify/cli-kit': minor
'@shopify/theme': minor
'@shopify/cli': minor
---

Add optional context name to `--development` flag in `theme push`

The `--development` flag now accepts an optional context name value (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.

Usage:
- `shopify theme push --development` (creates/uses unnamed development theme)
- `shopify theme push --development "PR-123"` (creates/reuses development theme named "PR-123")
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,12 @@ export interface themepush {
*/
'-d, --development'?: ''

/**
* 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.
* @environment SHOPIFY_FLAG_DEVELOPMENT_CONTEXT
*/
'-c, --development-context <value>'?: string

/**
* The environment to apply to the current command.
* @environment SHOPIFY_FLAG_ENVIRONMENT
Expand Down
11 changes: 10 additions & 1 deletion docs-shopify.dev/generated/generated_docs_data.json
Original file line number Diff line number Diff line change
Expand Up @@ -6938,6 +6938,15 @@
"isOptional": true,
"environmentValue": "SHOPIFY_FLAG_ALLOW_LIVE"
},
{
"filePath": "docs-shopify.dev/commands/interfaces/theme-push.interface.ts",
"syntaxKind": "PropertySignature",
"name": "-c, --development-context <value>",
"value": "string",
"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.",
"isOptional": true,
"environmentValue": "SHOPIFY_FLAG_DEVELOPMENT_CONTEXT"
},
{
"filePath": "docs-shopify.dev/commands/interfaces/theme-push.interface.ts",
"syntaxKind": "PropertySignature",
Expand Down Expand Up @@ -7038,7 +7047,7 @@
"environmentValue": "SHOPIFY_FLAG_IGNORE"
}
],
"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}"
"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}"
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
/* eslint-disable @typescript-eslint/consistent-type-definitions */
import * as Types from './types.js'

import {TypedDocumentNode as DocumentNode} from '@graphql-typed-document-node/core'

export type FindDevelopmentThemeByNameQueryVariables = Types.Exact<{
name: Types.Scalars['String']['input']
}>

export type FindDevelopmentThemeByNameQuery = {
themes?: {nodes: {id: string; name: string; role: Types.ThemeRole; processing: boolean}[]} | null
}

export const FindDevelopmentThemeByName = {
kind: 'Document',
definitions: [
{
kind: 'OperationDefinition',
operation: 'query',
name: {kind: 'Name', value: 'findDevelopmentThemeByName'},
variableDefinitions: [
{
kind: 'VariableDefinition',
variable: {kind: 'Variable', name: {kind: 'Name', value: 'name'}},
type: {kind: 'NonNullType', type: {kind: 'NamedType', name: {kind: 'Name', value: 'String'}}},
},
],
selectionSet: {
kind: 'SelectionSet',
selections: [
{
kind: 'Field',
name: {kind: 'Name', value: 'themes'},
arguments: [
{kind: 'Argument', name: {kind: 'Name', value: 'first'}, value: {kind: 'IntValue', value: '2'}},
{
kind: 'Argument',
name: {kind: 'Name', value: 'names'},
value: {kind: 'ListValue', values: [{kind: 'Variable', name: {kind: 'Name', value: 'name'}}]},
},
{
kind: 'Argument',
name: {kind: 'Name', value: 'roles'},
value: {kind: 'ListValue', values: [{kind: 'EnumValue', value: 'DEVELOPMENT'}]},
},
],
selectionSet: {
kind: 'SelectionSet',
selections: [
{
kind: 'Field',
name: {kind: 'Name', value: 'nodes'},
selectionSet: {
kind: 'SelectionSet',
selections: [
{kind: 'Field', name: {kind: 'Name', value: 'id'}},
{kind: 'Field', name: {kind: 'Name', value: 'name'}},
{kind: 'Field', name: {kind: 'Name', value: 'role'}},
{kind: 'Field', name: {kind: 'Name', value: 'processing'}},
{kind: 'Field', name: {kind: 'Name', value: '__typename'}},
],
},
},
{kind: 'Field', name: {kind: 'Name', value: '__typename'}},
],
},
},
],
},
},
],
} as unknown as DocumentNode<FindDevelopmentThemeByNameQuery, FindDevelopmentThemeByNameQueryVariables>
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
query findDevelopmentThemeByName($name: String!) {
themes(first: 2, names: [$name], roles: [DEVELOPMENT]) {
nodes {
id
name
role
processing
}
}
}
64 changes: 64 additions & 0 deletions packages/cli-kit/src/public/node/themes/api.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
themeDuplicate,
fetchTheme,
fetchThemes,
findDevelopmentThemeByName,
ThemeParams,
themeUpdate,
themePublish,
Expand All @@ -24,6 +25,7 @@ import {ThemeFilesUpsert} from '../../../cli/api/graphql/admin/generated/theme_f
import {ThemeFilesDelete} from '../../../cli/api/graphql/admin/generated/theme_files_delete.js'
import {GetThemes} from '../../../cli/api/graphql/admin/generated/get_themes.js'
import {GetTheme} from '../../../cli/api/graphql/admin/generated/get_theme.js'
import {FindDevelopmentThemeByName} from '../../../cli/api/graphql/admin/generated/find_development_theme_by_name.js'
import {adminRequestDoc, supportedApiVersions} from '../api/admin.js'
import {AbortError} from '../error.js'
import {test, vi, expect, describe, beforeEach} from 'vitest'
Expand Down Expand Up @@ -88,6 +90,68 @@ describe('fetchTheme', () => {
})
})

describe('findDevelopmentThemeByName', () => {
test('returns a development theme with a specific name', async () => {
vi.mocked(adminRequestDoc).mockResolvedValue({
themes: {
nodes: [{id: 'gid://shopify/OnlineStoreTheme/1', name: 'PR-123', role: 'DEVELOPMENT', processing: false}],
},
})

// When
const theme = await findDevelopmentThemeByName('PR-123', session)

// Then
expect(adminRequestDoc).toHaveBeenCalledWith({
query: FindDevelopmentThemeByName,
session,
variables: {name: 'PR-123'},
responseOptions: {handleErrors: false},
preferredBehaviour: expectedApiOptions,
})

expect(theme).not.toBeNull()
expect(theme!.id).toEqual(1)
expect(theme!.name).toEqual('PR-123')
expect(theme!.processing).toBeFalsy()
})

test('returns undefined when a theme is not found', async () => {
vi.mocked(adminRequestDoc).mockResolvedValue({
themes: {
nodes: [],
},
})

const theme = await findDevelopmentThemeByName('PR-123', session)

expect(theme).toBeUndefined()
})

test('aborts if there is more than one development theme with the same name', async () => {
vi.mocked(adminRequestDoc).mockResolvedValue({
themes: {
nodes: [
{id: 'gid://shopify/OnlineStoreTheme/1', name: 'PR-123', role: 'DEVELOPMENT', processing: true},
{id: 'gid://shopify/OnlineStoreTheme/2', name: 'PR-123', role: 'DEVELOPMENT', processing: true},
],
},
})

await expect(findDevelopmentThemeByName('PR-123', session)).rejects.toThrow(AbortError)
})

test('returns undefined when the query errors', async () => {
const errorResponse = {
status: 200,
errors: [{message: 'Theme not found'} as any],
}
vi.mocked(adminRequestDoc).mockRejectedValue(new ClientError(errorResponse, {query: ''}))

await expect(findDevelopmentThemeByName('PR-123', session)).rejects.toThrow()
})
})

describe('fetchThemes', () => {
test('returns store themes', async () => {
// Given
Expand Down
33 changes: 33 additions & 0 deletions packages/cli-kit/src/public/node/themes/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
import {MetafieldDefinitionsByOwnerType} from '../../../cli/api/graphql/admin/generated/metafield_definitions_by_owner_type.js'
import {GetThemes} from '../../../cli/api/graphql/admin/generated/get_themes.js'
import {GetTheme} from '../../../cli/api/graphql/admin/generated/get_theme.js'
import {FindDevelopmentThemeByName} from '../../../cli/api/graphql/admin/generated/find_development_theme_by_name.js'
import {OnlineStorePasswordProtection} from '../../../cli/api/graphql/admin/generated/online_store_password_protection.js'
import {RequestModeInput} from '../http.js'
import {adminRequestDoc} from '../api/admin.js'
Expand Down Expand Up @@ -113,6 +114,38 @@ export async function fetchThemes(session: AdminSession): Promise<Theme[]> {
}
}

export async function findDevelopmentThemeByName(name: string, session: AdminSession): Promise<Theme | undefined> {
recordEvent('theme-api:find-development-theme-by-name')

const {themes} = await adminRequestDoc({
query: FindDevelopmentThemeByName,
session,
variables: {name},
responseOptions: {handleErrors: false},
preferredBehaviour: THEME_API_NETWORK_BEHAVIOUR,
})

if (!themes) {
unexpectedGraphQLError('Failed to fetch themes')
}

if (themes.nodes.length > 1) {
throw recordError(new AbortError(`More than one development theme is named "${name}"`))
}

if (themes.nodes.length === 1) {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const {id, processing, role, name} = themes.nodes[0]!

return buildTheme({
id: parseGid(id),
processing,
role: role.toLowerCase(),
name,
})
}
}

export async function themeCreate(params: ThemeParams, session: AdminSession): Promise<Theme | undefined> {
const themeSource = params.src ?? SkeletonThemeCdn
recordEvent('theme-api:create-theme')
Expand Down
Loading
Loading