Skip to content

Add shopify app import channel-config for partner channel spec import - #8360

Merged
suzkiee merged 3 commits into
Shopify:mainfrom
suzkiee:suzkiee/channel-spec-generate-command
Oct 2, 2026
Merged

suzkiee merged 3 commits into
Shopify:mainfrom
suzkiee:suzkiee/channel-spec-generate-command

Conversation

@suzkiee

@suzkiee suzkiee commented Aug 21, 2026 •

Copy link
Copy Markdown
Contributor

WHY are these changes introduced?

Legacy channel apps need a migration path onto partner-owned channel_config extension specs. Shopify already has app-level FrozenRecord fallback specs for some legacy channels. Partners need a safe way to receive a deployable, partner-owned copy of the public parts of that spec.

The CLI team reviewed the proposal (thread) and approved this as a new import namespace under app:

start implementing this as import channel-config in Shopify CLI and we will work on a PR to stack on that moves the existing commands.

This PR adds app import channel-config and introduces the app import topic. A follow-up PR from the CLI team will migrate the existing import-extensions / import-custom-data-definitions commands into the same namespace.

WHAT is this pull request doing?

Adds a new command:

shopify app import channel-config [--stdout] [--overwrite]

Flow:

  1. Resolves the app from local context via linkedAppContext (standard --path / --client-id / --config / --reset flags).
  2. Calls a thin transport (services/import-channel-config/fetch.ts) that delegates to the canonical Channels-owned exporter (Channels::PartnerChannelSpecExport). The CLI never transforms or validates spec fields — the backend response is the deployable artifact.
  3. Writes extensions/channel-config/specifications/<handle>.toml (creates directories as needed).

Behavior details:

  • Refuses to overwrite an existing file unless --overwrite is passed.
  • --stdout prints only the TOML to stdout (pipeable); warnings go to stderr.
  • Backend warnings ({code, message}, e.g. automatic_product_feed_management) render out-of-band — never written into the TOML.
  • Failure reason codes map to partner-facing guidance: no_exportable_frozen_record, multiple_exportable_records, not_allowlisted, contains_no_public_fields, invalid_public_schema.
  • Never deploys; success output points at shopify app deploy as the next step.

Files:

File Purpose
packages/app/src/cli/commands/app/import/channel-config.ts oclif command (app:import:channel-config)
packages/app/src/cli/services/import-channel-config/import.ts file/stdout UX + failure mapping
packages/app/src/cli/services/import-channel-config/fetch.ts transport + response parsing
packages/app/src/cli/services/import-channel-config/import.test.ts 11 service tests (real temp dirs)
packages/app/src/cli/services/import-channel-config/fetch.test.ts 10 transport tests (GID URL, status handling, malformed bodies)
packages/app/src/cli/index.ts command registration
packages/cli/package.json new app:import topic description
Generated files oclif manifest, README, dev-docs data

Transport / backend contract

The command calls GET app_management/unstable/organizations/:orgId/apps/:id/channel_spec_export.json:

  • Success: {handle, filename, toml, warnings}
  • Well-formed failure: 422 {error: "not_exportable_yet", reason}
  • 404 is reserved for "endpoint unavailable / app not found" and surfaces as an abort.

The corresponding App Management endpoint is being added in shop/world (rework of shop/world#2051080).

Design constraints

  • Export is app-level and shop-independent — app-owner/collaborator auth only; no shop/admin/runtime auth contexts.
  • Internal-only fields are omitted entirely server-side via a public-field allowlist. Generated TOML must pass the public channel_config validation contract; the exporter fails closed otherwise.
  • Single-spec legacy apps only for MVP (multi-spec apps out of scope). A --specification-handle <handle> flag is reserved for future multi-spec support and intentionally not included here.
  • No comments, provenance, or warnings inside the TOML file.

@suzkiee suzkiee changed the title [Prototype] Add shopify app channel-spec generate for partner channel spec export Add shopify app import channel-config for partner channel spec import Sep 14, 2026
@suzkiee
suzkiee marked this pull request as ready for review September 16, 2026 14:51
@suzkiee
suzkiee requested review from a team as code owners September 16, 2026 14:51
@suzkiee
suzkiee force-pushed the suzkiee/channel-spec-generate-command branch from 791e783 to 6082b1e Compare September 17, 2026 19:34

@tauthomas01 tauthomas01 left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Looks good! I left some minor comments regarding the channel context. I am not the best person to review the CLI modules/architecture so it would be best to have someone from that team to review.

Comment thread packages/cli/README.md Resolved
suzkiee added a commit to suzkiee/cli that referenced this pull request Sep 22, 2026
- Mark the command hidden: the backend only serves an allowlisted
  cohort of channel partners, so keep it out of public help, README
  and the commands snapshot until the rollout widens.

- ensureExtensionConfig only checked that
  extensions/channel-config/shopify.extension.toml existed. If that
  file declared a different extension type, the imported spec was
  written under specifications/ and reported as success even though
  deploy would never bundle it as channel_config. Parse the existing
  file and abort with guidance unless type == "channel_config"; run
  the check before writing the spec so an abort leaves no stray file.

Addresses review feedback on Shopify#8360.

@tauthomas01 tauthomas01 left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Thank you for addressing my comments. Looks good from Channels side.

body = await response.json()
// eslint-disable-next-line no-catch-all/no-catch-all
} catch {
body = undefined

@isaacroldan isaacroldan Sep 29, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

if we can't parse the body, shouldn't we report it as ok: false regardless of what response.ok says? there was clearly an error somewhere

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Good point - updated to false!

@suzkiee
suzkiee force-pushed the suzkiee/channel-spec-generate-command branch from 09ad398 to fa25686 Compare September 29, 2026 18:57
suzkiee added a commit to suzkiee/cli that referenced this pull request Sep 29, 2026
- Mark the command hidden: the backend only serves an allowlisted
  cohort of channel partners, so keep it out of public help, README
  and the commands snapshot until the rollout widens.

- ensureExtensionConfig only checked that
  extensions/channel-config/shopify.extension.toml existed. If that
  file declared a different extension type, the imported spec was
  written under specifications/ and reported as success even though
  deploy would never bundle it as channel_config. Parse the existing
  file and abort with guidance unless type == "channel_config"; run
  the check before writing the spec so an abort leaves no stray file.

Addresses review feedback on Shopify#8360.
@suzkiee
suzkiee force-pushed the suzkiee/channel-spec-generate-command branch from fa25686 to f8f253c Compare September 29, 2026 19:04

const EXTENSION_CONFIG_FILENAME = 'shopify.extension.toml'
const EXTENSION_CONFIG_CONTENT = 'name = "Channel config"\ntype = "channel_config"\nhandle = "channel-config"\n'
const CHANNEL_CONFIG_EXTENSION_TYPE = 'channel_config'

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Better to define this one in the specification in channel.ts and import it here (because the value is hardcoded there too)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

channel.ts exports CHANNEL_CONFIG_INDENTIFIER now and we're now importing here.

return {extensionDirectory: existing[0]!.directory, createExtension: false}
}

const extensionDirectories = app.configuration.extension_directories ?? DEFAULT_EXTENSION_DIRECTORIES

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

you need to check here if app.configuration.extension_directories not only exists but is also not empty.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I fixed the check to check for the empty case and added a test case as well.

@suzkiee
suzkiee requested a review from isaacroldan October 1, 2026 17:22
Imports the Shopify-authored channel spec for a linked app as a
deployable channel_config extension, so partners migrating off
Shopify-managed specs get a ready-to-edit TOML instead of authoring one
from the docs.

The command calls the App Management endpoint added in shop/world#2051080
(GET /app_management/:version/organizations/:org/apps/:app/channel_spec_export.json)
through a new DeveloperPlatformClient.channelSpecExport method, then
writes specifications/<handle>.toml into the app's channel_config
extension:

- reuses the existing channel_config extension if the app has one,
  wherever it lives and whichever TOML layout it uses
- otherwise scaffolds extensions/channel-config, but aborts before
  writing if extension_directories wouldn't discover it or another
  extension already occupies that directory
- --force to overwrite an existing spec; --json for machine output

Status handling mirrors the server contract: 200 success, 422 with a
reason code for well-formed "not exportable" failures, 404 as
endpoint/app unavailable, 426 as outdated CLI, 401/403 as auth.

The command is hidden while the backend is allowlist-gated to a small
cohort of channel partners.
…as default

- Export CHANNEL_CONFIG_IDENTIFIER and CHANNEL_CONFIG_SPECIFICATIONS_DIRECTORY
  from the channel spec and use them in the import command instead of
  duplicating the literals.
- Fall back to extensions/* when extension_directories is an empty array,
  matching the app loader, instead of aborting with an empty directory list.
@suzkiee
suzkiee force-pushed the suzkiee/channel-spec-generate-command branch from 0d5fe02 to c906c26 Compare October 2, 2026 14:45
if (!wouldBeDiscovered) {
throw new AbortError(
`Your shopify.app.toml only loads extensions from ${extensionDirectories.join(', ')}, so a new extension at ${CHANNEL_SPEC_EXTENSION_DIRECTORY} wouldn't be deployed.`,
`Create a ${CHANNEL_CONFIG_IDENTIFIER} extension in one of those directories with a ${EXTENSION_CONFIG_FILENAME} like:\n\n${EXTENSION_CONFIG_CONTENT}\nthen re-run this command.`,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

We could also print here the full channel config we downloaded, since we are asking them to create a file with some content, why not use the real content directly? or will it be too big?

@isaacroldan isaacroldan left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I have not tophatted, but code looks good

@suzkiee

suzkiee commented Oct 2, 2026

Copy link
Copy Markdown
Contributor Author

I ran this locally from the PR branch against our team's dogfooding app:

Screen.Recording.2026-10-02.at.1.34.25.PM.mov
  1. Happy path: app import channel-config fetches the Shopify authored default and writes a minimally scaffolded TOML file
  2. Overwrite guard: Re-running the command aborts with a message that the spec already exists. It leaves the existing file untouched.
  • Using --force overwrites the spec and reuses the existing channe_config extension instead of creating a second one.
  • Using --json emits {handle, filename, path, toml, warnings} for machine consumption.

@suzkiee

suzkiee commented Oct 2, 2026

Copy link
Copy Markdown
Contributor Author

/snapit

@github-actions

github-actions Bot commented Oct 2, 2026

Copy link
Copy Markdown
Contributor

Unable to request a snapshot: Only users with write permission to the repository can run /snapit.

@suzkiee
suzkiee added this pull request to the merge queue Oct 2, 2026
Merged via the queue into Shopify:main with commit 7e4e6ac Oct 2, 2026
29 checks passed
@suzkiee
suzkiee deleted the suzkiee/channel-spec-generate-command branch October 2, 2026 18:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants