Skip to content

Add app subscription migration commands - #8399

Open
tyler-eon wants to merge 5 commits into
mainfrom
eon/subscription-migrations
Open

Add app subscription migration commands#8399
tyler-eon wants to merge 5 commits into
mainfrom
eon/subscription-migrations

Conversation

@tyler-eon

@tyler-eon tyler-eon commented Aug 25, 2026

Copy link
Copy Markdown

Why

Partners migrating legacy manual-billing subscriptions to Shopify-managed app pricing need a safe, scriptable CLI workflow.

What changed

Adds four core Shopify CLI commands:

  • shopify app subscription-migrations schedule
  • shopify app subscription-migrations unschedule
  • shopify app subscription-migrations status
  • shopify app subscription-migrations cancel

The commands:

  • use the standard linked-app context, with an optional --client-id override;
  • accept migration CSVs through --input, stdin, or --input -;
  • validate the complete CSV before making a network mutation;
  • normalize and sort shop GIDs, then create deterministic batches of at most 250 shops;
  • generate a portable root idempotency key and deterministic per-batch keys;
  • preserve accepted operation GIDs and structured failures in one typed result;
  • support repeated --id flags for multi-batch status and cancellation;
  • show visible polling progress for watched submissions and status checks;
  • keep JSON stdout to one machine-readable document while polling progress is rendered on stderr.

cancel stops an operation from processing additional shops. It does not undo shops that have already been scheduled or migrated; unschedule is the separate reversal workflow.

Top-hatting

Caution

These commands change real app-subscription migration state. Use only a controlled test app and shop, and confirm the intended plan and shop before submitting.

Prerequisites

  1. Use a controlled app that is initially configured for manual billing.
  2. Install the app on a controlled test shop and create/approve a recurring manual-billing subscription.
  3. Configure the app's intended Shopify App Pricing target plan and record its plan handle.
  4. Switch the app to Shopify App Pricing so the legacy subscription is eligible for migration.
  5. Confirm the Partners CLI migration schema and required backend rollout gates are available for the app/organization.
  6. Check out this branch and install/build Shopify CLI with a supported Node version.
pnpm install
pnpm --filter @shopify/app build
pnpm --filter @shopify/cli build

Run commands from the app project. To select another accessible app explicitly, add --client-id <client-id>.

Schedule one subscription

Create /tmp/subscription-migrations.csv:

shop_id,target_plan_handle,price_behavior,notification
gid://shopify/Shop/123456789,target-plan-handle,HONOR_BILLING_PRICE,WHEN_REQUIRED

Submit and watch the operation:

pnpm shopify app subscription-migrations schedule \
  --input /tmp/subscription-migrations.csv \
  --force \
  --watch

Preserve the root idempotency key and every operation GID printed by the command. COMPLETED means processing is terminal; inspect the per-shop result code before treating the migration as successful.

Check status

pnpm shopify app subscription-migrations status \
  --id 'gid://shopify/AppSubscriptionMigrationOperation/123' \
  --watch

Repeat --id for every operation returned by a multi-batch submission. Add --json to verify that stdout contains one structured document while progress remains on stderr.

Cancel unprocessed work

pnpm shopify app subscription-migrations cancel \
  --id 'gid://shopify/AppSubscriptionMigrationOperation/123'

Cancellation prevents additional shops from being processed. It does not reverse shops that were already scheduled or migrated.

Unschedule a reversible migration

You can reuse /tmp/subscription-migrations.csv from earlier. As long as the input has a shop_id column, it will correctly create an unscheduling job. Or, you can create a separate file /tmp/subscription-migrations-unschedule.csv:

shop_id
gid://shopify/Shop/123456789

Then run:

pnpm shopify app subscription-migrations unschedule \
  --input /tmp/subscription-migrations-unschedule.csv \
  --force \
  --watch

Only run this while the migration remains reversible.

Backend dependency

Uses the Partners CLI GraphQL fields introduced by Shopify/partners#63097:

  • appSubscriptionMigrationOperationCreate
  • appSubscriptionMigrationOperation
  • appSubscriptionMigrationOperationCancel

The app Client ID is a selector rather than authorization. Partners derives organization membership from the authenticated CLI session and enforces migration permissions and app scoping.

Testing

  • pnpm vitest run packages/app/src/cli — 2,258 tests passed
  • focused subscription migration suite — 168 tests passed
  • pnpm --filter @shopify/app lint
  • pnpm --filter @shopify/app type-check
  • pnpm --filter @shopify/app build
  • pnpm knip
  • CLI command discovery and help for all four commands
  • command-tree snapshot verification
  • malformed file/stdin CSV validation before authentication or network access
  • generated README, oclif manifest, and Shopify.dev command documentation verification

Assisted-By: devx/bb753bc6-d4c5-4a52-8d97-044f4b1a3497
@github-actions github-actions Bot added the Area: @shopify/cli @shopify/cli package issues label Aug 25, 2026
@tyler-eon
tyler-eon marked this pull request as ready for review August 25, 2026 19:14
@tyler-eon
tyler-eon requested review from a team as code owners August 25, 2026 19:14
Assisted-By: devx/bb753bc6-d4c5-4a52-8d97-044f4b1a3497

@dmerand dmerand 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 think there are a couple of minor changes we can make to structure here to better-align with repo guidance + also prepare these commands for incoming changes.

Context: Shopify CLI is moving finite commands toward typed result contracts. In that model, command execution returns structured domain data, while a CLI adapter decides whether to render terminal text or encode JSON. This lets the same execution support other adapters later and ensures --json emits exactly one document, including when useful data exists alongside an unsuccessful outcome.

Comment thread packages/app/src/cli/services/subscription-migrations/submit-migration-plan.ts Outdated
Comment thread packages/app/src/cli/services/subscription-migrations/cancel-operations.ts Outdated
Comment thread packages/app/src/cli/utilities/developer-platform-client/partners-client.ts Outdated
Comment thread .changeset/quiet-badgers-poll.md Outdated
Assisted-By: devx/bb753bc6-d4c5-4a52-8d97-044f4b1a3497
@tyler-eon

Copy link
Copy Markdown
Author

@dmerand

Submission and cancellation execution now return typed domain results, while command-local codecs and presenters own JSON/text rendering and exit status. Expected data-bearing failures produce one structured result and one JSON document; protocol/invariant failures remain exceptions. I believe this work should now align better with the document you referenced.

@dmerand
dmerand requested a review from gonzaloriestra August 26, 2026 18:55

@gonzaloriestra gonzaloriestra 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.

Could you share tophatting instructions? A demo would be useful as well.

Also, the PR is too large, next time please split in a stack 🙏

Comment thread packages/app/src/cli/commands/app/subscription-migrations/schedule.ts Outdated
Comment thread packages/app/src/cli/services/subscription-migrations/run-submission-command.ts Outdated
Assisted-By: devx/bb753bc6-d4c5-4a52-8d97-044f4b1a3497
Assisted-By: devx/bb753bc6-d4c5-4a52-8d97-044f4b1a3497
@tyler-eon
tyler-eon force-pushed the eon/subscription-migrations branch from 6b7cc93 to 661e68c Compare August 27, 2026 18:07
@gonzaloriestra

Copy link
Copy Markdown
Contributor

/snapit

@github-actions

Copy link
Copy Markdown
Contributor

🫰✨ Thanks @gonzaloriestra! Your snapshot has been published to npm.

Test the snapshot by installing your package globally:

pnpm i -g --@shopify:registry=https://registry.npmjs.org @shopify/cli@0.0.0-snapshot-20260828075026

Caution

After installing, validate the version by running shopify version in your terminal.
If the versions don't match, you might have multiple global instances installed.
Use which shopify to find out which one you are running and uninstall it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Area: @shopify/cli @shopify/cli package issues

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants