Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
99875ad
Add app static_root to extensibility websocket watch list
melissaluu Mar 30, 2026
34bbcc0
Added tests and fix a few callouts from LLM review
melissaluu Apr 1, 2026
1b6ad75
Remove unnecessary export from AppAssets interface
melissaluu Apr 1, 2026
a349d95
Address PR feedback: use getAppAssets callback and fix debounce timer…
melissaluu Apr 1, 2026
5ce5d81
Hook app asset file watcher into the existing app event watcher list …
melissaluu Apr 2, 2026
6fb614b
Fix tests and remove unnecessary early return
melissaluu Apr 2, 2026
e22d316
Add release notes for 3.93
lizkenyon Apr 2, 2026
937c354
Update RELEASE_NOTES/3.93.md
lizkenyon Apr 2, 2026
d8a434f
Update RELEASE_NOTES/3.93.md
lizkenyon Apr 2, 2026
12cd80f
Update RELEASE_NOTES/3.93.md
lizkenyon Apr 2, 2026
b26fa23
Remove unused @babel/core and eslint-plugin-jsx-a11y deps from eslint…
ryancbahan Apr 2, 2026
69df450
Remove @opentelemetry/semantic-conventions dependency
ryancbahan Apr 2, 2026
c173151
Remove commondir dependency, inline as commonParentDirectory()
ryancbahan Apr 2, 2026
0ec88b9
Remove get-port-please, use Node net module directly
ryancbahan Apr 2, 2026
01f72a7
Remove react-router-dom, render Extensions directly
ryancbahan Apr 2, 2026
e565970
Remove debug dependency from eslint-plugin-cli
ryancbahan Apr 3, 2026
3cfbe29
Remove tempy dependency, use Node builtins for temp directories
ryancbahan Apr 3, 2026
ea31be1
Remove eslint-plugin-jsdoc from eslint-plugin-cli
ryancbahan Apr 3, 2026
4b45d1c
Remove terminal-link, use existing ansi-escapes and supports-hyperlinks
ryancbahan Apr 3, 2026
5dcc411
test: add regression tests for clientSteps passthrough in specificati…
craigmichaelmartin Apr 1, 2026
58f48d0
Make store auth scopes additive
dmerand Apr 2, 2026
b94371e
Stop Playwright tracing during E2E login to prevent credential exposure
ryancbahan Apr 3, 2026
6cb9a06
Replace fill() with evaluate() to prevent credential capture in traces
ryancbahan Apr 3, 2026
c3c911c
Fix lint and type errors in browser-login
ryancbahan Apr 3, 2026
dbf8127
Fix remaining type and lint errors
ryancbahan Apr 3, 2026
bdf5e44
Clear login page on auth failure to prevent credential capture
ryancbahan Apr 3, 2026
20534ec
add monorail recording
ryancbahan Apr 6, 2026
318f36e
Store auth restructure
dmerand Apr 3, 2026
239e75a
store execute restructure
dmerand Apr 3, 2026
cc775f8
Add/enforce --json for store commands
dmerand Apr 3, 2026
ba40871
Use App object to resolve app assets
melissaluu Apr 8, 2026
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
19 changes: 19 additions & 0 deletions RELEASE_NOTES/3.93.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
## App

- Add `shopify organization list` command to list Shopify organizations you have access to, supports `--json` flag for structured output [#6956](https://github.com/Shopify/cli/pull/6956)
- Add `shopify store auth` and `shopify store execute` commands. `store auth` stores per-user online auth for a store using PKCE. `store execute` runs Admin API GraphQL against that stored auth; mutations are disabled by default and write operations require `--allow-mutations` [#7122](https://github.com/Shopify/cli/pull/7122)
- Add support for `SHOPIFY_APP_AUTOMATION_TOKEN` env var as a new name for `SHOPIFY_CLI_PARTNERS_TOKEN` [#7057](https://github.com/Shopify/cli/pull/7057)
- Enable non-interactive `app init` via a new `--organization-id` flag and not prompting to link to an existing app if `--name` is provided [#6640](https://github.com/Shopify/cli/pull/6640)
- Deprecation warning for `--force` flag on `app deploy` and `app release`. Use `--allow-updates` for CI/CD environments, or `--allow-updates --allow-deletes` if you also want to allow removals [#7059](https://github.com/Shopify/cli/pull/7059)
- Fix crash when organization is not found in app-management-client [#7012](https://github.com/Shopify/cli/pull/7012)

## Theme

- Add `--development-context` flag to `theme push` to specify a unique identifier for a development theme context (e.g., PR number, branch name), useful for CI environments [#6657](https://github.com/Shopify/cli/pull/6657)
- Add support for theme previews using a JSON via `theme preview`. Pass a JSON via `--override` to quickly preview overrides on a live theme. Also adds a `--preview-id` flag to handle in-place updates for previews created from an override JSON [#6890](https://github.com/Shopify/cli/pull/6890)
- Add `--json` flag to `theme preview` to configure a json output [#7043](https://github.com/Shopify/cli/pull/7043)
- Change wording for current development theme in `theme list` from `[yours]` to `[current]` to reflect support for multiple development themes [#6657](https://github.com/Shopify/cli/pull/6657)
- Fix theme editor shortcut tracking fetch requests instead of page navigation [#6924](https://github.com/Shopify/cli/pull/6924)
- Add a 250ms debounce on filewatcher for themes to stop potential file deletes [#6791](https://github.com/Shopify/cli/pull/6791)
- Fix Cart rate limiting issue [#6975](https://github.com/Shopify/cli/pull/6975)
- Fix missing json output for `theme info` when no theme or dev flag is present [#6905](https://github.com/Shopify/cli/pull/6905)
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
// This is an autogenerated file. Don't edit this file manually.
export interface storeauth {
/**
* Output the result as JSON. Automatically disables color output.
* @environment SHOPIFY_FLAG_JSON
*/
'-j, --json'?: ''

/**
* Disable color output.
* @environment SHOPIFY_FLAG_NO_COLOR
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,12 @@ export interface storeexecute {
*/
'--allow-mutations'?: ''

/**
* Output the result as JSON. Automatically disables color output.
* @environment SHOPIFY_FLAG_JSON
*/
'-j, --json'?: ''

/**
* Disable color output.
* @environment SHOPIFY_FLAG_NO_COLOR
Expand Down
22 changes: 20 additions & 2 deletions docs-shopify.dev/generated/generated_docs_data.json
Original file line number Diff line number Diff line change
Expand Up @@ -5829,6 +5829,15 @@
"isOptional": true,
"environmentValue": "SHOPIFY_FLAG_VERBOSE"
},
{
"filePath": "docs-shopify.dev/commands/interfaces/store-auth.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/store-auth.interface.ts",
"syntaxKind": "PropertySignature",
Expand All @@ -5838,7 +5847,7 @@
"environmentValue": "SHOPIFY_FLAG_STORE"
}
],
"value": "export interface storeauth {\n /**\n * Disable color output.\n * @environment SHOPIFY_FLAG_NO_COLOR\n */\n '--no-color'?: ''\n\n /**\n * Comma-separated Admin API scopes to request for the app.\n * @environment SHOPIFY_FLAG_SCOPES\n */\n '--scopes <value>': string\n\n /**\n * The myshopify.com domain of the store to authenticate against.\n * @environment SHOPIFY_FLAG_STORE\n */\n '-s, --store <value>': string\n\n /**\n * Increase the verbosity of the output.\n * @environment SHOPIFY_FLAG_VERBOSE\n */\n '--verbose'?: ''\n}"
"value": "export interface storeauth {\n /**\n * Output the result as JSON. Automatically disables color output.\n * @environment SHOPIFY_FLAG_JSON\n */\n '-j, --json'?: ''\n\n /**\n * Disable color output.\n * @environment SHOPIFY_FLAG_NO_COLOR\n */\n '--no-color'?: ''\n\n /**\n * Comma-separated Admin API scopes to request for the app.\n * @environment SHOPIFY_FLAG_SCOPES\n */\n '--scopes <value>': string\n\n /**\n * The myshopify.com domain of the store to authenticate against.\n * @environment SHOPIFY_FLAG_STORE\n */\n '-s, --store <value>': string\n\n /**\n * Increase the verbosity of the output.\n * @environment SHOPIFY_FLAG_VERBOSE\n */\n '--verbose'?: ''\n}"
}
}
}
Expand Down Expand Up @@ -5938,6 +5947,15 @@
"isOptional": true,
"environmentValue": "SHOPIFY_FLAG_VERSION"
},
{
"filePath": "docs-shopify.dev/commands/interfaces/store-execute.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/store-execute.interface.ts",
"syntaxKind": "PropertySignature",
Expand Down Expand Up @@ -5965,7 +5983,7 @@
"environmentValue": "SHOPIFY_FLAG_VARIABLES"
}
],
"value": "export interface storeexecute {\n /**\n * Allow GraphQL mutations to run against the target store.\n * @environment SHOPIFY_FLAG_ALLOW_MUTATIONS\n */\n '--allow-mutations'?: ''\n\n /**\n * Disable color output.\n * @environment SHOPIFY_FLAG_NO_COLOR\n */\n '--no-color'?: ''\n\n /**\n * The file name where results should be written, instead of STDOUT.\n * @environment SHOPIFY_FLAG_OUTPUT_FILE\n */\n '--output-file <value>'?: string\n\n /**\n * The GraphQL query or mutation, as a string.\n * @environment SHOPIFY_FLAG_QUERY\n */\n '-q, --query <value>'?: string\n\n /**\n * Path to a file containing the GraphQL query or mutation. Can't be used with --query.\n * @environment SHOPIFY_FLAG_QUERY_FILE\n */\n '--query-file <value>'?: string\n\n /**\n * The myshopify.com domain of the store to execute against.\n * @environment SHOPIFY_FLAG_STORE\n */\n '-s, --store <value>': string\n\n /**\n * Path to a file containing GraphQL variables in JSON format. Can't be used with --variables.\n * @environment SHOPIFY_FLAG_VARIABLE_FILE\n */\n '--variable-file <value>'?: string\n\n /**\n * The values for any GraphQL variables in your query or mutation, in JSON format.\n * @environment SHOPIFY_FLAG_VARIABLES\n */\n '-v, --variables <value>'?: string\n\n /**\n * Increase the verbosity of the output.\n * @environment SHOPIFY_FLAG_VERBOSE\n */\n '--verbose'?: ''\n\n /**\n * The API version to use for the query or mutation. Defaults to the latest stable version.\n * @environment SHOPIFY_FLAG_VERSION\n */\n '--version <value>'?: string\n}"
"value": "export interface storeexecute {\n /**\n * Allow GraphQL mutations to run against the target store.\n * @environment SHOPIFY_FLAG_ALLOW_MUTATIONS\n */\n '--allow-mutations'?: ''\n\n /**\n * Output the result as JSON. Automatically disables color output.\n * @environment SHOPIFY_FLAG_JSON\n */\n '-j, --json'?: ''\n\n /**\n * Disable color output.\n * @environment SHOPIFY_FLAG_NO_COLOR\n */\n '--no-color'?: ''\n\n /**\n * The file name where results should be written, instead of STDOUT.\n * @environment SHOPIFY_FLAG_OUTPUT_FILE\n */\n '--output-file <value>'?: string\n\n /**\n * The GraphQL query or mutation, as a string.\n * @environment SHOPIFY_FLAG_QUERY\n */\n '-q, --query <value>'?: string\n\n /**\n * Path to a file containing the GraphQL query or mutation. Can't be used with --query.\n * @environment SHOPIFY_FLAG_QUERY_FILE\n */\n '--query-file <value>'?: string\n\n /**\n * The myshopify.com domain of the store to execute against.\n * @environment SHOPIFY_FLAG_STORE\n */\n '-s, --store <value>': string\n\n /**\n * Path to a file containing GraphQL variables in JSON format. Can't be used with --variables.\n * @environment SHOPIFY_FLAG_VARIABLE_FILE\n */\n '--variable-file <value>'?: string\n\n /**\n * The values for any GraphQL variables in your query or mutation, in JSON format.\n * @environment SHOPIFY_FLAG_VARIABLES\n */\n '-v, --variables <value>'?: string\n\n /**\n * Increase the verbosity of the output.\n * @environment SHOPIFY_FLAG_VERBOSE\n */\n '--verbose'?: ''\n\n /**\n * The API version to use for the query or mutation. Defaults to the latest stable version.\n * @environment SHOPIFY_FLAG_VERSION\n */\n '--version <value>'?: string\n}"
}
}
}
Expand Down
4 changes: 4 additions & 0 deletions eslint.config.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import nxPlugin from '@nx/eslint-plugin'
import cliPlugin from '@shopify/eslint-plugin-cli'
import jsdocPlugin from 'eslint-plugin-jsdoc'

// Spread the CLI plugin's base config which includes all necessary plugins
const config = [
Expand Down Expand Up @@ -44,6 +45,9 @@ const config = [
'**/public/node/result.ts',
'**/public/node/themes/**/*',
],
plugins: {
jsdoc: jsdocPlugin,
},
settings: {
jsdoc: {
publicFunctionsOnly: true,
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@
"bugsnag-build-reporter": "^2.0.0",
"commander": "^9.4.0",
"esbuild": "0.27.4",
"eslint-plugin-jsdoc": "50.7.1",
"eslint": "^9.26.0",
"execa": "^7.2.0",
"fast-glob": "3.3.3",
Expand Down
95 changes: 95 additions & 0 deletions packages/app/src/cli/commands/app/config/validate.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ import {testAppLinked} from '../../../models/app/app.test-data.js'
import {Project} from '../../../models/project/project.js'
import {selectActiveConfig} from '../../../models/project/active-config.js'
import {errorsForConfig} from '../../../models/project/config-selection.js'
import metadata from '../../../metadata.js'
import {outputResult} from '@shopify/cli-kit/node/output'
import {AbortError} from '@shopify/cli-kit/node/error'
import {TomlFile} from '@shopify/cli-kit/node/toml/toml-file'
import {describe, expect, test, vi} from 'vitest'

Expand All @@ -14,12 +16,19 @@ vi.mock('../../../services/validate.js')
vi.mock('../../../models/project/project.js')
vi.mock('../../../models/project/active-config.js')
vi.mock('../../../models/project/config-selection.js')
vi.mock('../../../metadata.js', () => ({default: {addPublicMetadata: vi.fn()}}))
vi.mock('@shopify/cli-kit/node/output', async (importOriginal) => {
const actual = await importOriginal<typeof import('@shopify/cli-kit/node/output')>()
return {...actual, outputResult: vi.fn()}
})
vi.mock('@shopify/cli-kit/node/ui')

async function expectValidationMetadataCalls(...expectedMetadata: Record<string, unknown>[]) {
const metadataCalls = vi.mocked(metadata.addPublicMetadata).mock.calls.map(([getMetadata]) => getMetadata)
expect(metadataCalls).toHaveLength(expectedMetadata.length)
await expect(Promise.all(metadataCalls.map((getMetadata) => getMetadata()))).resolves.toEqual(expectedMetadata)
}

function mockHealthyProject() {
vi.mocked(Project.load).mockResolvedValue({errors: []} as unknown as Project)
vi.mocked(selectActiveConfig).mockResolvedValue({file: new TomlFile('shopify.app.toml', {})} as any)
Expand All @@ -36,6 +45,7 @@ describe('app config validate command', () => {
await Validate.run([], import.meta.url)

expect(validateApp).toHaveBeenCalledWith(app, {json: false})
await expectValidationMetadataCalls({cmd_app_validate_json: false})
})

test('calls validateApp with json: true when --json flag is passed', async () => {
Expand All @@ -47,6 +57,7 @@ describe('app config validate command', () => {
await Validate.run(['--json'], import.meta.url)

expect(validateApp).toHaveBeenCalledWith(app, {json: true})
await expectValidationMetadataCalls({cmd_app_validate_json: true})
})

test('calls validateApp with json: true when -j flag is passed', async () => {
Expand All @@ -58,6 +69,7 @@ describe('app config validate command', () => {
await Validate.run(['-j'], import.meta.url)

expect(validateApp).toHaveBeenCalledWith(app, {json: true})
await expectValidationMetadataCalls({cmd_app_validate_json: true})
})

test('outputs JSON issues when active config has TOML parse errors', async () => {
Expand All @@ -71,5 +83,88 @@ describe('app config validate command', () => {

expect(outputResult).toHaveBeenCalledWith(expect.stringContaining('"valid": false'))
expect(linkedAppContext).not.toHaveBeenCalled()
await expectValidationMetadataCalls(
{cmd_app_validate_json: true},
{
cmd_app_validate_valid: false,
cmd_app_validate_issue_count: 1,
cmd_app_validate_file_count: 1,
},
)
})

test('records failure metadata for config errors in non-json mode', async () => {
vi.mocked(Project.load).mockResolvedValue({errors: []} as unknown as Project)
vi.mocked(selectActiveConfig).mockResolvedValue({file: new TomlFile('shopify.app.toml', {})} as any)
vi.mocked(errorsForConfig).mockReturnValue([
{path: '/app/shopify.app.toml', message: 'Missing required field'} as any,
{path: '/app/shopify.app.toml', message: 'Invalid value'} as any,
])

await expect(Validate.run([], import.meta.url)).rejects.toThrow()

expect(linkedAppContext).not.toHaveBeenCalled()
await expectValidationMetadataCalls(
{cmd_app_validate_json: false},
{
cmd_app_validate_valid: false,
cmd_app_validate_issue_count: 2,
cmd_app_validate_file_count: 1,
},
)
})

test('records failure metadata when Project.load fails with --json', async () => {
vi.mocked(Project.load).mockRejectedValue(new AbortError('Could not find app configuration'))

await expect(Validate.run(['--json'], import.meta.url)).rejects.toThrow()

expect(outputResult).toHaveBeenCalledWith(expect.stringContaining('"valid": false'))
expect(selectActiveConfig).not.toHaveBeenCalled()
await expectValidationMetadataCalls(
{cmd_app_validate_json: true},
{
cmd_app_validate_valid: false,
cmd_app_validate_issue_count: 1,
cmd_app_validate_file_count: 1,
},
)
})

test('records failure metadata when selectActiveConfig fails with --json', async () => {
vi.mocked(Project.load).mockResolvedValue({errors: []} as unknown as Project)
vi.mocked(selectActiveConfig).mockRejectedValue(new AbortError('No config found'))

await expect(Validate.run(['--json'], import.meta.url)).rejects.toThrow()

expect(outputResult).toHaveBeenCalledWith(expect.stringContaining('"valid": false'))
expect(linkedAppContext).not.toHaveBeenCalled()
await expectValidationMetadataCalls(
{cmd_app_validate_json: true},
{
cmd_app_validate_valid: false,
cmd_app_validate_issue_count: 1,
cmd_app_validate_file_count: 1,
},
)
})

test('records failure metadata when linkedAppContext throws a validation error with --json', async () => {
vi.mocked(Project.load).mockResolvedValue({errors: []} as unknown as Project)
vi.mocked(selectActiveConfig).mockResolvedValue({file: new TomlFile('shopify.app.toml', {})} as any)
vi.mocked(errorsForConfig).mockReturnValue([])
vi.mocked(linkedAppContext).mockRejectedValue(new AbortError('Validation errors in /app/shopify.app.toml'))

await expect(Validate.run(['--json'], import.meta.url)).rejects.toThrow()

expect(outputResult).toHaveBeenCalledWith(expect.stringContaining('"valid": false'))
await expectValidationMetadataCalls(
{cmd_app_validate_json: true},
{
cmd_app_validate_valid: false,
cmd_app_validate_issue_count: 1,
cmd_app_validate_file_count: 1,
},
)
})
})
18 changes: 18 additions & 0 deletions packages/app/src/cli/commands/app/config/validate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,20 @@ import {linkedAppContext} from '../../../services/app-context.js'
import {selectActiveConfig} from '../../../models/project/active-config.js'
import {errorsForConfig} from '../../../models/project/config-selection.js'
import {Project} from '../../../models/project/project.js'
import metadata from '../../../metadata.js'
import {globalFlags, jsonFlag} from '@shopify/cli-kit/node/cli'
import {AbortError, AbortSilentError} from '@shopify/cli-kit/node/error'
import {outputResult, stringifyMessage, unstyled} from '@shopify/cli-kit/node/output'
import {renderError} from '@shopify/cli-kit/node/ui'

async function recordValidationFailure(issueCount: number, fileCount: number) {
await metadata.addPublicMetadata(() => ({
cmd_app_validate_valid: false,
cmd_app_validate_issue_count: issueCount,
cmd_app_validate_file_count: fileCount,
}))
}

export default class Validate extends AppLinkedCommand {
static summary = 'Validate your app configuration and extensions.'

Expand All @@ -26,12 +35,17 @@ export default class Validate extends AppLinkedCommand {
public async run(): Promise<AppLinkedCommandOutput> {
const {flags} = await this.parse(Validate)

await metadata.addPublicMetadata(() => ({
cmd_app_validate_json: flags.json,
}))

// Stage 1: Load project
let project: Project
try {
project = await Project.load(flags.path)
} catch (err) {
if (err instanceof AbortError && flags.json) {
await recordValidationFailure(1, 1)
const message = unstyled(stringifyMessage(err.message)).trim()
outputResult(JSON.stringify({valid: false, issues: [{message}]}, null, 2))
throw new AbortSilentError()
Expand All @@ -45,6 +59,7 @@ export default class Validate extends AppLinkedCommand {
activeConfig = await selectActiveConfig(project, flags.config)
} catch (err) {
if (err instanceof AbortError && flags.json) {
await recordValidationFailure(1, 1)
const message = unstyled(stringifyMessage(err.message)).trim()
outputResult(JSON.stringify({valid: false, issues: [{message}]}, null, 2))
throw new AbortSilentError()
Expand All @@ -55,6 +70,8 @@ export default class Validate extends AppLinkedCommand {
const configErrors = errorsForConfig(project, activeConfig.file)
if (configErrors.length > 0) {
const issues = configErrors.map((err) => ({file: err.path, message: err.message}))
const fileCount = new Set(configErrors.map((err) => err.path)).size
await recordValidationFailure(issues.length, fileCount)
if (flags.json) {
outputResult(JSON.stringify({valid: false, issues}, null, 2))
throw new AbortSilentError()
Expand Down Expand Up @@ -83,6 +100,7 @@ export default class Validate extends AppLinkedCommand {
const message = err instanceof AbortError ? unstyled(stringifyMessage(err.message)).trim() : ''
const isValidationError = message.startsWith('Validation errors in ')
if (isValidationError && flags.json) {
await recordValidationFailure(1, 1)
outputResult(JSON.stringify({valid: false, issues: [{message}]}, null, 2))
throw new AbortSilentError()
}
Expand Down
Loading
Loading