Skip to content
Draft
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
92 changes: 92 additions & 0 deletions docs-shopify.dev/generated/generated_docs_data_v2.json
Original file line number Diff line number Diff line change
Expand Up @@ -411,6 +411,98 @@
"value": "export interface appbulkstatus {\n /**\n * Alias of the Shopify account to use for authentication.\n * @environment SHOPIFY_FLAG_AUTH_ALIAS\n */\n '--auth-alias <value>'?: string\n\n /**\n * The Client ID of your app.\n * @environment SHOPIFY_FLAG_CLIENT_ID\n */\n '--client-id <value>'?: string\n\n /**\n * The name of the app configuration.\n * @environment SHOPIFY_FLAG_APP_CONFIG\n */\n '-c, --config <value>'?: string\n\n /**\n * The bulk operation ID (numeric ID or full GID). If not provided, lists all bulk operations belonging to this app on this store in the last 7 days.\n * @environment SHOPIFY_FLAG_ID\n */\n '--id <value>'?: string\n\n /**\n * Disable color output.\n * @environment SHOPIFY_FLAG_NO_COLOR\n */\n '--no-color'?: ''\n\n /**\n * The path to your app directory.\n * @environment SHOPIFY_FLAG_PATH\n */\n '--path <value>'?: string\n\n /**\n * Reset all your settings.\n * @environment SHOPIFY_FLAG_RESET\n */\n '--reset'?: ''\n\n /**\n * The store domain. Must be an existing dev store.\n * @environment SHOPIFY_FLAG_STORE\n */\n '-s, --store <value>'?: string\n\n /**\n * Increase the verbosity of the output. May include sensitive data.\n * @environment SHOPIFY_FLAG_VERBOSE\n */\n '--verbose'?: ''\n}"
}
},
"appchannelspecgenerate": {
"docs-shopify.dev/commands/interfaces/app-channel-spec-generate.interface.ts": {
"filePath": "docs-shopify.dev/commands/interfaces/app-channel-spec-generate.interface.ts",
"name": "appchannelspecgenerate",
"description": "The following flags are available for the `app channel-spec generate` command:",
"isPublicDocs": true,
"members": [
{
"filePath": "docs-shopify.dev/commands/interfaces/app-channel-spec-generate.interface.ts",
"syntaxKind": "PropertySignature",
"name": "--auth-alias <value>",
"value": "string",
"description": "Alias of the Shopify account to use for authentication.",
"isOptional": true,
"environmentValue": "SHOPIFY_FLAG_AUTH_ALIAS"
},
{
"filePath": "docs-shopify.dev/commands/interfaces/app-channel-spec-generate.interface.ts",
"syntaxKind": "PropertySignature",
"name": "--client-id <value>",
"value": "string",
"description": "The Client ID of your app.",
"isOptional": true,
"environmentValue": "SHOPIFY_FLAG_CLIENT_ID"
},
{
"filePath": "docs-shopify.dev/commands/interfaces/app-channel-spec-generate.interface.ts",
"syntaxKind": "PropertySignature",
"name": "--no-color",
"value": "''",
"description": "Disable color output.",
"isOptional": true,
"environmentValue": "SHOPIFY_FLAG_NO_COLOR"
},
{
"filePath": "docs-shopify.dev/commands/interfaces/app-channel-spec-generate.interface.ts",
"syntaxKind": "PropertySignature",
"name": "--overwrite",
"value": "''",
"description": "Overwrite the existing channel spec file if one already exists.",
"isOptional": true,
"environmentValue": "SHOPIFY_FLAG_OVERWRITE"
},
{
"filePath": "docs-shopify.dev/commands/interfaces/app-channel-spec-generate.interface.ts",
"syntaxKind": "PropertySignature",
"name": "--path <value>",
"value": "string",
"description": "The path to your app directory.",
"isOptional": true,
"environmentValue": "SHOPIFY_FLAG_PATH"
},
{
"filePath": "docs-shopify.dev/commands/interfaces/app-channel-spec-generate.interface.ts",
"syntaxKind": "PropertySignature",
"name": "--reset",
"value": "''",
"description": "Reset all your settings.",
"isOptional": true,
"environmentValue": "SHOPIFY_FLAG_RESET"
},
{
"filePath": "docs-shopify.dev/commands/interfaces/app-channel-spec-generate.interface.ts",
"syntaxKind": "PropertySignature",
"name": "--stdout",
"value": "''",
"description": "Print the generated TOML to stdout instead of writing it to a file.",
"isOptional": true,
"environmentValue": "SHOPIFY_FLAG_STDOUT"
},
{
"filePath": "docs-shopify.dev/commands/interfaces/app-channel-spec-generate.interface.ts",
"syntaxKind": "PropertySignature",
"name": "--verbose",
"value": "''",
"description": "Increase the verbosity of the output. May include sensitive data.",
"isOptional": true,
"environmentValue": "SHOPIFY_FLAG_VERBOSE"
},
{
"filePath": "docs-shopify.dev/commands/interfaces/app-channel-spec-generate.interface.ts",
"syntaxKind": "PropertySignature",
"name": "-c, --config <value>",
"value": "string",
"description": "The name of the app configuration.",
"isOptional": true,
"environmentValue": "SHOPIFY_FLAG_APP_CONFIG"
}
],
"value": "export interface appchannelspecgenerate {\n /**\n * Alias of the Shopify account to use for authentication.\n * @environment SHOPIFY_FLAG_AUTH_ALIAS\n */\n '--auth-alias <value>'?: string\n\n /**\n * The Client ID of your app.\n * @environment SHOPIFY_FLAG_CLIENT_ID\n */\n '--client-id <value>'?: string\n\n /**\n * The name of the app configuration.\n * @environment SHOPIFY_FLAG_APP_CONFIG\n */\n '-c, --config <value>'?: string\n\n /**\n * Disable color output.\n * @environment SHOPIFY_FLAG_NO_COLOR\n */\n '--no-color'?: ''\n\n /**\n * Overwrite the existing channel spec file if one already exists.\n * @environment SHOPIFY_FLAG_OVERWRITE\n */\n '--overwrite'?: ''\n\n /**\n * The path to your app directory.\n * @environment SHOPIFY_FLAG_PATH\n */\n '--path <value>'?: string\n\n /**\n * Reset all your settings.\n * @environment SHOPIFY_FLAG_RESET\n */\n '--reset'?: ''\n\n /**\n * Print the generated TOML to stdout instead of writing it to a file.\n * @environment SHOPIFY_FLAG_STDOUT\n */\n '--stdout'?: ''\n\n /**\n * Increase the verbosity of the output. May include sensitive data.\n * @environment SHOPIFY_FLAG_VERBOSE\n */\n '--verbose'?: ''\n}"
}
},
"appconfiglink": {
"docs-shopify.dev/commands/interfaces/app-config-link.interface.ts": {
"filePath": "docs-shopify.dev/commands/interfaces/app-config-link.interface.ts",
Expand Down
52 changes: 52 additions & 0 deletions packages/app/src/cli/commands/app/channel-spec/generate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import {appFlags} from '../../../flags.js'
import {linkedAppContext} from '../../../services/app-context.js'
import {generateChannelSpec} from '../../../services/channel-spec/generate.js'
import AppLinkedCommand, {AppLinkedCommandOutput} from '../../../utilities/app-linked-command.js'
import {Flags} from '@oclif/core'
import {globalFlags} from '@shopify/cli-kit/node/cli'

export default class ChannelSpecGenerate extends AppLinkedCommand {
static summary = 'Generate a channel spec TOML file from the Shopify-authored default.'

static descriptionWithMarkdown = `Generates a deployable \`channel_config\` extension spec from the Shopify-authored default channel specification for your app.

The generated TOML file contains only public \`channel_config\` fields. Review it, commit it to your app, then deploy it with \`shopify app deploy\`. This command never deploys the spec itself.`

static description = this.descriptionWithoutMarkdown()

static flags = {
...globalFlags,
...appFlags,
stdout: Flags.boolean({
description: 'Print the generated TOML to stdout instead of writing it to a file.',
env: 'SHOPIFY_FLAG_STDOUT',
default: false,
}),
overwrite: Flags.boolean({
description: 'Overwrite the existing channel spec file if one already exists.',
env: 'SHOPIFY_FLAG_OVERWRITE',
default: false,
}),
}

public async run(): Promise<AppLinkedCommandOutput> {
const {flags} = await this.parse(ChannelSpecGenerate)

const {app, remoteApp, developerPlatformClient} = await linkedAppContext({
directory: flags.path,
clientId: flags['client-id'],
forceRelink: flags.reset,
userProvidedConfigName: flags.config,
})

await generateChannelSpec({
app,
remoteApp,
developerPlatformClient,
stdout: flags.stdout,
overwrite: flags.overwrite,
})

return {app}
}
}
2 changes: 2 additions & 0 deletions packages/app/src/cli/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import Build from './commands/app/build.js'
import BulkCancel from './commands/app/bulk/cancel.js'
import BulkStatus from './commands/app/bulk/status.js'
import ChannelSpecGenerate from './commands/app/channel-spec/generate.js'
import ConfigLink from './commands/app/config/link.js'
import ConfigUse from './commands/app/config/use.js'
import ConfigPull from './commands/app/config/pull.js'
Expand Down Expand Up @@ -45,6 +46,7 @@ export const commands: {[key: string]: typeof AppLinkedCommand | typeof AppUnlin
'app:build': Build,
'app:bulk:cancel': BulkCancel,
'app:bulk:status': BulkStatus,
'app:channel-spec:generate': ChannelSpecGenerate,
'app:deploy': Deploy,
'app:dev': Dev,
'app:dev:clean': DevClean,
Expand Down
100 changes: 100 additions & 0 deletions packages/app/src/cli/services/channel-spec/fetch.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import {OrganizationApp} from '../../models/organization.js'
import {DeveloperPlatformClient} from '../../utilities/developer-platform-client.js'
import {appManagementHeaders} from '@shopify/cli-kit/node/api/app-management'
import {appManagementFqdn} from '@shopify/cli-kit/node/context/fqdn'
import {shopifyFetch} from '@shopify/cli-kit/node/http'
import {AbortError} from '@shopify/cli-kit/node/error'

export interface ChannelSpecExportWarning {
code: string
message: string
}

export type ChannelSpecExportResult =
| {
success: true
handle: string
filename: string
toml: string
warnings: ChannelSpecExportWarning[]
}
| {
success: false
reason: string
}

interface FetchChannelSpecExportOptions {
remoteApp: OrganizationApp
developerPlatformClient: DeveloperPlatformClient
}

/**
* Fetches the partner-safe channel spec export for an app.
*
* The export itself is produced server-side by the Channels-owned exporter, which projects the
* Shopify-authored default channel specification into the public channel_config schema and
* validates it before returning it. The CLI intentionally does not transform or validate the
* TOML locally: the backend response is the deployable artifact.
*/
export async function fetchChannelSpecExport({
remoteApp,
developerPlatformClient,
}: FetchChannelSpecExportOptions): Promise<ChannelSpecExportResult> {
const fqdn = await appManagementFqdn()
const url = `https://${fqdn}/app_management/unstable/organizations/${remoteApp.organizationId}/apps/${remoteApp.id}/channel_spec_export.json`
const token = (await developerPlatformClient.session()).token

const response = await shopifyFetch(url, {
method: 'GET',
headers: appManagementHeaders(token),
})

if (response.status === 404) {
// A 404 is not part of the export contract (failures are 422 with a reason code). It means the
// export endpoint isn't available (not deployed yet), or the app/organization couldn't be found.
throw new AbortError(
'The channel spec export endpoint is not available for this app.',
'Confirm the app and organization are correct, and that the channel spec export backend is available.',
)
}

let payload: {[key: string]: unknown}
try {
payload = (await response.json()) as {[key: string]: unknown}
} catch {
throw new AbortError(`Failed to fetch the channel spec export: unexpected response (status ${response.status}).`)
}

if (!response.ok) {
const reason = typeof payload.reason === 'string' ? payload.reason : `http_${response.status}`
return {success: false, reason}
}

const {handle, filename, toml, warnings} = payload
if (typeof handle !== 'string' || typeof filename !== 'string' || typeof toml !== 'string') {
throw new AbortError('Failed to fetch the channel spec export: the response was missing required fields.')
}

return {
success: true,
handle,
filename,
toml,
warnings: parseWarnings(warnings),
}
}

function parseWarnings(warnings: unknown): ChannelSpecExportWarning[] {
if (!Array.isArray(warnings)) return []
return warnings.flatMap((warning) => {
if (
warning &&
typeof warning === 'object' &&
typeof (warning as {code?: unknown}).code === 'string' &&
typeof (warning as {message?: unknown}).message === 'string'
) {
return [warning as ChannelSpecExportWarning]
}
return []
})
}
Loading
Loading