From 76ac9ea5cfea2829b803c7fa1d85db7fe2f2623b Mon Sep 17 00:00:00 2001 From: Gonzalo Riestra Date: Fri, 18 Sep 2026 14:18:20 +0200 Subject: [PATCH] Add JSON Schema for subscription migration scheduling Co-Authored-By: Claude Fable 5 --- .../subscription-migrations/commands.test.ts | 15 +++++-- .../result-codec.test.ts | 34 ++++++++++++-- .../subscription-migrations/result-codec.ts | 5 ++- .../result-presenter-output.test.ts | 34 +++++++++++++- .../result-presenter.test.ts | 4 +- .../result-presenter.ts | 4 +- .../app/subscription-migrations/schedule.ts | 8 +++- .../run-submission-command.test.ts | 2 +- .../run-submission-command.ts | 3 +- .../submit-migration-plan.ts | 32 ++----------- .../services/subscription-migrations/types.ts | 45 +++++++++++++++++++ packages/cli/oclif.manifest.json | 2 +- .../rules/json-output-command-exceptions.js | 1 - 13 files changed, 141 insertions(+), 48 deletions(-) diff --git a/packages/app/src/cli/commands/app/subscription-migrations/commands.test.ts b/packages/app/src/cli/commands/app/subscription-migrations/commands.test.ts index 0d31fa9ad25..e1d8db0875f 100644 --- a/packages/app/src/cli/commands/app/subscription-migrations/commands.test.ts +++ b/packages/app/src/cli/commands/app/subscription-migrations/commands.test.ts @@ -11,6 +11,7 @@ import {cancelMigrationOperations} from '../../../services/subscription-migratio import { migrationCancellationJsonOutputSchema, migrationListJsonOutputSchema, + migrationSubmissionJsonOutputSchema, } from '../../../services/subscription-migrations/types.js' import {outputOperations} from '../../../services/subscription-migrations/command-output.js' import {getMigrationOperations} from '../../../services/subscription-migrations/get-operations.js' @@ -22,8 +23,10 @@ import {outputResult} from '@shopify/cli-kit/node/output' import {renderSuccess, renderWarning} from '@shopify/cli-kit/node/ui' import {afterEach, beforeEach, describe, expect, test, vi} from 'vitest' import type {MigrationOperation} from '../../../models/subscription-migrations.js' -import type {MigrationCancellationResult} from '../../../services/subscription-migrations/types.js' -import type {MigrationSubmissionResult} from '../../../services/subscription-migrations/submit-migration-plan.js' +import type { + MigrationCancellationResult, + MigrationSubmissionResult, +} from '../../../services/subscription-migrations/types.js' vi.mock('../../../services/app-context.js') vi.mock('../../../services/subscription-migrations/cancel-operations.js', async (importOriginal) => ({ @@ -440,6 +443,12 @@ describe('subscription migration command metadata', () => { expect(List.description).toContain('```json') }) + test('schedule exposes and documents the submission JSON output schema', () => { + expect(Schedule.jsonOutputSchema).toBe(migrationSubmissionJsonOutputSchema) + expect(Schedule.description).toContain('`MigrationSubmissionResult` schema') + expect(Schedule.description).toContain('```json') + }) + test('cancel exposes its JSON output schema', () => { expect(Cancel.jsonOutputSchema).toBe(migrationCancellationJsonOutputSchema) }) @@ -563,7 +572,7 @@ describe('subscription migration command metadata', () => { }, ) - test.each([Schedule, Unschedule, Status])('$name has no fenced-code markers in its plain description', (Command) => { + test.each([Unschedule, Status])('$name has no fenced-code markers in its plain description', (Command) => { expect(Command.description).not.toContain('```') }) diff --git a/packages/app/src/cli/commands/app/subscription-migrations/result-codec.test.ts b/packages/app/src/cli/commands/app/subscription-migrations/result-codec.test.ts index 7c974514a65..2313c773393 100644 --- a/packages/app/src/cli/commands/app/subscription-migrations/result-codec.test.ts +++ b/packages/app/src/cli/commands/app/subscription-migrations/result-codec.test.ts @@ -1,12 +1,15 @@ import {encodeMigrationCancellationResult, encodeMigrationSubmissionResult} from './result-codec.js' -import {migrationCancellationJsonOutputSchema} from '../../../services/subscription-migrations/types.js' +import { + migrationCancellationJsonOutputSchema, + migrationSubmissionJsonOutputSchema, +} from '../../../services/subscription-migrations/types.js' import {describe, expect, test} from 'vitest' import type {MigrationOperation} from '../../../models/subscription-migrations.js' -import type {MigrationCancellationResult} from '../../../services/subscription-migrations/types.js' import type { + MigrationCancellationResult, MigrationSubmission, MigrationSubmissionResult, -} from '../../../services/subscription-migrations/submit-migration-plan.js' +} from '../../../services/subscription-migrations/types.js' function operation(id: string): MigrationOperation { return {id, status: 'RUNNING', total: 1, results: {edges: []}} @@ -124,3 +127,28 @@ describe('subscription migration result codecs', () => { expect(document).toBe(JSON.stringify({outcomes: result.outcomes}, null, 2)) }) }) + +describe('migration submission JSON contract', () => { + test('preserves an empty successful submission without failure details', () => { + const value = {...submission(), total: 0, operations: []} + expect(encodeMigrationSubmissionResult({status: 'success', submission: value})).toBe(JSON.stringify(value, null, 2)) + }) + + test('preserves total submission failure with nullable error fields', () => { + const value = {...submission(), operations: []} + const failure = {type: 'submission' as const, batchIndex: 0, userErrors: [{message: 'Rejected', field: null}]} + expect(encodeMigrationSubmissionResult({status: 'failed', submission: value, failure})).toBe( + JSON.stringify({...value, failure}, null, 2), + ) + }) + + test.each([ + {action: 'cancel'}, + {total: '1'}, + {failure: {type: 'operations'}}, + {failure: {type: 'submission', batchIndex: 0, userErrors: [{message: 'Rejected'}]}}, + {operations: [{batchIndex: 0, batchPayloadDigest: 'digest', operation: {...operation('one'), status: 'UNKNOWN'}}]}, + ])('rejects invalid submission fields: %j', (fields) => { + expect(() => migrationSubmissionJsonOutputSchema.validate({...submission(), ...fields})).toThrow() + }) +}) diff --git a/packages/app/src/cli/commands/app/subscription-migrations/result-codec.ts b/packages/app/src/cli/commands/app/subscription-migrations/result-codec.ts index 2f47cea98ab..c40ca719ac4 100644 --- a/packages/app/src/cli/commands/app/subscription-migrations/result-codec.ts +++ b/packages/app/src/cli/commands/app/subscription-migrations/result-codec.ts @@ -1,8 +1,9 @@ import { migrationCancellationJsonOutputSchema, + migrationSubmissionJsonOutputSchema, type MigrationCancellationResult, + type MigrationSubmissionResult, } from '../../../services/subscription-migrations/types.js' -import type {MigrationSubmissionResult} from '../../../services/subscription-migrations/submit-migration-plan.js' export function encodeMigrationSubmissionResult(result: MigrationSubmissionResult): string { const document = @@ -12,7 +13,7 @@ export function encodeMigrationSubmissionResult(result: MigrationSubmissionResul ...result.submission, failure: result.failure, } - return JSON.stringify(document, null, 2) + return migrationSubmissionJsonOutputSchema.encode(document) } export function encodeMigrationCancellationResult(result: MigrationCancellationResult): string { diff --git a/packages/app/src/cli/commands/app/subscription-migrations/result-presenter-output.test.ts b/packages/app/src/cli/commands/app/subscription-migrations/result-presenter-output.test.ts index 996ca473a02..2a550047410 100644 --- a/packages/app/src/cli/commands/app/subscription-migrations/result-presenter-output.test.ts +++ b/packages/app/src/cli/commands/app/subscription-migrations/result-presenter-output.test.ts @@ -1,4 +1,4 @@ -import {presentMigrationCancellationResult} from './result-presenter.js' +import {presentMigrationCancellationResult, presentMigrationSubmissionResult} from './result-presenter.js' import {beforeEach, describe, expect, test, vi} from 'vitest' const isUnitTest = vi.hoisted(() => vi.fn(() => false)) @@ -41,3 +41,35 @@ describe('migration cancellation JSON output', () => { } }) }) + +describe('migration submission JSON output', () => { + test.each([false, true])('writes one final failure document with watch=%s', (watch) => { + const stdout = vi.spyOn(process.stdout, 'write').mockImplementation(() => true) + const stderr = vi.spyOn(process.stderr, 'write').mockImplementation(() => true) + const submission = { + clientId: 'client-id', + action: 'schedule' as const, + inputDigest: 'input-digest', + total: 2, + operations: [ + { + batchIndex: 0, + batchPayloadDigest: 'batch-digest', + operation: {id: 'operation-one', status: 'RUNNING' as const, total: 1, results: {edges: []}}, + }, + ], + } + const failure = {type: 'submission' as const, batchIndex: 1, userErrors: [{message: 'Rejected', field: null}]} + + try { + expect(presentMigrationSubmissionResult({status: 'failed', submission, failure}, {json: true, watch})).toBe(1) + + expect(stdout).toHaveBeenCalledOnce() + expect(stdout.mock.calls[0]?.[0]).toBe(`${JSON.stringify({...submission, failure}, null, 2)}\n`) + expect(stderr).not.toHaveBeenCalled() + } finally { + stdout.mockRestore() + stderr.mockRestore() + } + }) +}) diff --git a/packages/app/src/cli/commands/app/subscription-migrations/result-presenter.test.ts b/packages/app/src/cli/commands/app/subscription-migrations/result-presenter.test.ts index ecc7d579d95..6cb38ebddea 100644 --- a/packages/app/src/cli/commands/app/subscription-migrations/result-presenter.test.ts +++ b/packages/app/src/cli/commands/app/subscription-migrations/result-presenter.test.ts @@ -7,11 +7,11 @@ import {outputResult} from '@shopify/cli-kit/node/output' import {renderInfo, renderSuccess, renderWarning} from '@shopify/cli-kit/node/ui' import {beforeEach, describe, expect, test, vi} from 'vitest' import type {MigrationOperation} from '../../../models/subscription-migrations.js' -import type {MigrationCancellationResult} from '../../../services/subscription-migrations/types.js' import type { + MigrationCancellationResult, MigrationSubmission, MigrationSubmissionResult, -} from '../../../services/subscription-migrations/submit-migration-plan.js' +} from '../../../services/subscription-migrations/types.js' vi.mock('@shopify/cli-kit/node/output', async (importOriginal) => { const actual = await importOriginal() diff --git a/packages/app/src/cli/commands/app/subscription-migrations/result-presenter.ts b/packages/app/src/cli/commands/app/subscription-migrations/result-presenter.ts index edf644986ed..b2628b64bbc 100644 --- a/packages/app/src/cli/commands/app/subscription-migrations/result-presenter.ts +++ b/packages/app/src/cli/commands/app/subscription-migrations/result-presenter.ts @@ -5,11 +5,9 @@ import type {MigrationOperation} from '../../../models/subscription-migrations.j import type { MigrationCancellationOutcome, MigrationCancellationResult, -} from '../../../services/subscription-migrations/types.js' -import type { MigrationSubmission, MigrationSubmissionResult, -} from '../../../services/subscription-migrations/submit-migration-plan.js' +} from '../../../services/subscription-migrations/types.js' interface SubmissionPresentationOptions { json: boolean diff --git a/packages/app/src/cli/commands/app/subscription-migrations/schedule.ts b/packages/app/src/cli/commands/app/subscription-migrations/schedule.ts index 3bc13394e7b..ed828e6dab7 100644 --- a/packages/app/src/cli/commands/app/subscription-migrations/schedule.ts +++ b/packages/app/src/cli/commands/app/subscription-migrations/schedule.ts @@ -1,8 +1,10 @@ import {submissionFlags} from './flags.js' import {presentAcceptedMigrationSubmission, presentMigrationSubmissionResult} from './result-presenter.js' +import {migrationSubmissionJsonOutputSchema} from '../../../services/subscription-migrations/types.js' import {linkedAppContext} from '../../../services/app-context.js' import {runSubmissionCommand} from '../../../services/subscription-migrations/run-submission-command.js' import AppLinkedCommand, {AppLinkedCommandOutput} from '../../../utilities/app-linked-command.js' +import {jsonFlag} from '@shopify/cli-kit/node/cli' export default class Schedule extends AppLinkedCommand { static hidden = true @@ -35,7 +37,11 @@ Use \`--force\` to skip confirmation and immediately submit every valid row. Wit '<%= config.bin %> <%= command.id %> --input - --force --watch', ] - static flags = {...submissionFlags} + static flags = {...submissionFlags, ...jsonFlag} + + static get jsonOutputSchema() { + return migrationSubmissionJsonOutputSchema + } async run(): Promise { const {flags} = await this.parse(Schedule) diff --git a/packages/app/src/cli/services/subscription-migrations/run-submission-command.test.ts b/packages/app/src/cli/services/subscription-migrations/run-submission-command.test.ts index 1c53a8aa1b4..c53572be2bf 100644 --- a/packages/app/src/cli/services/subscription-migrations/run-submission-command.test.ts +++ b/packages/app/src/cli/services/subscription-migrations/run-submission-command.test.ts @@ -6,7 +6,7 @@ import {AbortError, AbortSilentError} from '@shopify/cli-kit/node/error' import {renderConfirmationPrompt} from '@shopify/cli-kit/node/ui' import {beforeEach, describe, expect, test, vi} from 'vitest' import type {MigrationOperation} from '../../models/subscription-migrations.js' -import type {MigrationSubmission, MigrationSubmissionResult} from './submit-migration-plan.js' +import type {MigrationSubmission, MigrationSubmissionResult} from './types.js' vi.mock('./plan/plan-migration-input.js') vi.mock('./submit-migration-plan.js', async (importOriginal) => { diff --git a/packages/app/src/cli/services/subscription-migrations/run-submission-command.ts b/packages/app/src/cli/services/subscription-migrations/run-submission-command.ts index ffdf3e7d2be..3547f7a5978 100644 --- a/packages/app/src/cli/services/subscription-migrations/run-submission-command.ts +++ b/packages/app/src/cli/services/subscription-migrations/run-submission-command.ts @@ -1,8 +1,9 @@ import {planMigrationInput} from './plan/plan-migration-input.js' -import {submitMigrationPlan, type MigrationSubmission, type MigrationSubmissionResult} from './submit-migration-plan.js' +import {submitMigrationPlan} from './submit-migration-plan.js' import {watchMigrationOperations} from './watch-operations.js' import {AbortError, AbortSilentError} from '@shopify/cli-kit/node/error' import {renderConfirmationPrompt} from '@shopify/cli-kit/node/ui' +import type {MigrationSubmission, MigrationSubmissionResult} from './types.js' import type { MigrationAction, MigrationOperation, diff --git a/packages/app/src/cli/services/subscription-migrations/submit-migration-plan.ts b/packages/app/src/cli/services/subscription-migrations/submit-migration-plan.ts index bbe5d1ebd22..16940506187 100644 --- a/packages/app/src/cli/services/subscription-migrations/submit-migration-plan.ts +++ b/packages/app/src/cli/services/subscription-migrations/submit-migration-plan.ts @@ -1,33 +1,7 @@ -import {createMigrationOperation, type MigrationApiInput, type MigrationUserError} from './partners-api.js' +import {createMigrationOperation, type MigrationApiInput} from './partners-api.js' import {deriveBatchIdempotencyKey, generateInvocationId} from './plan/idempotency.js' -import type { - MigrationOperation, - MigrationPlan, - PlannedMigrationRow, - ScheduledMigrationRow, -} from '../../models/subscription-migrations.js' - -export interface SubmittedMigrationOperation { - batchIndex: number - batchPayloadDigest: string - operation: MigrationOperation -} - -export interface MigrationSubmission { - clientId: string - action: MigrationPlan['action'] - inputDigest: string - total: number - operations: SubmittedMigrationOperation[] -} - -export type MigrationSubmissionFailure = - | {type: 'submission'; batchIndex: number; userErrors: MigrationUserError[]} - | {type: 'operations'; operationIds: string[]} - -export type MigrationSubmissionResult = - | {status: 'success'; submission: MigrationSubmission} - | {status: 'failed'; submission: MigrationSubmission; failure: MigrationSubmissionFailure} +import type {MigrationSubmission, MigrationSubmissionResult} from './types.js' +import type {MigrationPlan, PlannedMigrationRow, ScheduledMigrationRow} from '../../models/subscription-migrations.js' export class MigrationSubmissionProtocolError extends Error { readonly batchIndex: number diff --git a/packages/app/src/cli/services/subscription-migrations/types.ts b/packages/app/src/cli/services/subscription-migrations/types.ts index 40c02817321..e7c8531df6f 100644 --- a/packages/app/src/cli/services/subscription-migrations/types.ts +++ b/packages/app/src/cli/services/subscription-migrations/types.ts @@ -99,3 +99,48 @@ export const migrationListJsonOutputSchema = defineJsonOutputSchema({ MigratableSubscriptionNotification: MigratableSubscriptionNotificationSchema, }, }) + +const SubmittedMigrationOperationSchema = zod.object({ + batchIndex: zod.number(), + batchPayloadDigest: zod.string(), + operation: MigrationOperationSchema, +}) + +const MigrationSubmissionFailureSchema = zod.discriminatedUnion('type', [ + zod.object({ + type: zod.literal('submission'), + batchIndex: zod.number(), + userErrors: zod.array(MigrationUserErrorSchema), + }), + zod.object({ + type: zod.literal('operations'), + operationIds: zod.array(zod.string()), + }), +]) + +export const migrationSubmissionJsonOutputSchema = defineJsonOutputSchema({ + name: 'MigrationSubmissionResult', + schema: zod.object({ + clientId: zod.string(), + action: zod.enum(['schedule', 'unschedule']), + inputDigest: zod.string(), + total: zod.number(), + operations: zod.array(SubmittedMigrationOperationSchema), + failure: MigrationSubmissionFailureSchema.optional(), + }), + definitions: { + SubmittedMigrationOperation: SubmittedMigrationOperationSchema, + MigrationOperation: MigrationOperationSchema, + MigrationOperationResultEdge: MigrationOperationResultEdgeSchema, + MigrationOperationResultNode: MigrationOperationResultNodeSchema, + MigrationSubmissionFailure: MigrationSubmissionFailureSchema, + MigrationUserError: MigrationUserErrorSchema, + }, +}) + +export type MigrationSubmissionJsonOutput = InferJsonOutputSchema +export type MigrationSubmission = Omit +type MigrationSubmissionFailure = NonNullable +export type MigrationSubmissionResult = + | {status: 'success'; submission: MigrationSubmission} + | {status: 'failed'; submission: MigrationSubmission; failure: MigrationSubmissionFailure} diff --git a/packages/cli/oclif.manifest.json b/packages/cli/oclif.manifest.json index 5cde981338f..b07b642bc93 100644 --- a/packages/cli/oclif.manifest.json +++ b/packages/cli/oclif.manifest.json @@ -4223,7 +4223,7 @@ "args": { }, "customPluginName": "@shopify/app", - "description": "Schedules manual-billing subscriptions to migrate to Shopify-managed app pricing.\n\nWhen `--input` is omitted, the command reads CSV data from stdin. Use `--input ` to read from a file. `--input -` is also supported as an explicit stdin path.\n\n- Required CSV columns: `shop_id`, `target_plan_handle`, and `price_behavior`.\n- Optional CSV column: `notification`.\n- Example header: `shop_id,target_plan_handle,price_behavior,notification`.\n- Example row: `123456789,pro,HONOR_BILLING_PRICE,WHEN_REQUIRED`.\n\n`price_behavior` must be `HONOR_BILLING_PRICE` or `PLAN_PRICE`. `notification` can be `OPT_OUT` or `WHEN_REQUIRED` and defaults to `WHEN_REQUIRED` when omitted or blank.\n\nValidation is atomic: the command submits no operations unless the entire CSV is valid. Valid rows are submitted in batches of 250 shops. Preserve every operation GID printed by the command so you can check or cancel the submitted operations.\n\nRun the command from an app project. By default, it uses the Client ID from the active app configuration. Use `--path` to select an app directory or `--config` to select a configuration. Pass `--client-id` to select a different app within the project. Use `--reset` to relink the app.\n\nUse `--force` to skip confirmation and immediately submit every valid row. With `--watch`, human-readable output shows accepted identifiers before polling begins, then displays operation progress and the final outcome. With `--json --watch`, the command outputs one structured JSON document after every operation reaches a terminal status.", + "description": "Schedules manual-billing subscriptions to migrate to Shopify-managed app pricing.\n\nWhen `--input` is omitted, the command reads CSV data from stdin. Use `--input ` to read from a file. `--input -` is also supported as an explicit stdin path.\n\n- Required CSV columns: `shop_id`, `target_plan_handle`, and `price_behavior`.\n- Optional CSV column: `notification`.\n- Example header: `shop_id,target_plan_handle,price_behavior,notification`.\n- Example row: `123456789,pro,HONOR_BILLING_PRICE,WHEN_REQUIRED`.\n\n`price_behavior` must be `HONOR_BILLING_PRICE` or `PLAN_PRICE`. `notification` can be `OPT_OUT` or `WHEN_REQUIRED` and defaults to `WHEN_REQUIRED` when omitted or blank.\n\nValidation is atomic: the command submits no operations unless the entire CSV is valid. Valid rows are submitted in batches of 250 shops. Preserve every operation GID printed by the command so you can check or cancel the submitted operations.\n\nRun the command from an app project. By default, it uses the Client ID from the active app configuration. Use `--path` to select an app directory or `--config` to select a configuration. Pass `--client-id` to select a different app within the project. Use `--reset` to relink the app.\n\nUse `--force` to skip confirmation and immediately submit every valid row. With `--watch`, human-readable output shows accepted identifiers before polling begins, then displays operation progress and the final outcome. With `--json --watch`, the command outputs one structured JSON document after every operation reaches a terminal status.\n\nOutput from `--json` conforms to the `MigrationSubmissionResult` schema.\n\nUse `--json-schema` to print the result, error, and event schemas.\n\n```json\n{\n \"type\": \"object\",\n \"properties\": {\n \"clientId\": {\n \"type\": \"string\"\n },\n \"action\": {\n \"type\": \"string\",\n \"enum\": [\n \"schedule\",\n \"unschedule\"\n ]\n },\n \"inputDigest\": {\n \"type\": \"string\"\n },\n \"total\": {\n \"type\": \"number\"\n },\n \"operations\": {\n \"type\": \"array\",\n \"items\": {\n \"$ref\": \"#/definitions/SubmittedMigrationOperation\"\n }\n },\n \"failure\": {\n \"$ref\": \"#/definitions/MigrationSubmissionFailure\"\n }\n },\n \"required\": [\n \"clientId\",\n \"action\",\n \"inputDigest\",\n \"total\",\n \"operations\"\n ],\n \"additionalProperties\": false,\n \"title\": \"MigrationSubmissionResult\",\n \"definitions\": {\n \"SubmittedMigrationOperation\": {\n \"type\": \"object\",\n \"properties\": {\n \"batchIndex\": {\n \"type\": \"number\"\n },\n \"batchPayloadDigest\": {\n \"type\": \"string\"\n },\n \"operation\": {\n \"$ref\": \"#/definitions/MigrationOperation\"\n }\n },\n \"required\": [\n \"batchIndex\",\n \"batchPayloadDigest\",\n \"operation\"\n ],\n \"additionalProperties\": false\n },\n \"MigrationOperation\": {\n \"type\": \"object\",\n \"properties\": {\n \"id\": {\n \"type\": \"string\"\n },\n \"status\": {\n \"type\": \"string\",\n \"enum\": [\n \"RUNNING\",\n \"COMPLETED\",\n \"FAILED\",\n \"CANCELED\"\n ]\n },\n \"total\": {\n \"type\": \"number\"\n },\n \"results\": {\n \"type\": \"object\",\n \"properties\": {\n \"edges\": {\n \"type\": \"array\",\n \"items\": {\n \"$ref\": \"#/definitions/MigrationOperationResultEdge\"\n }\n }\n },\n \"required\": [\n \"edges\"\n ],\n \"additionalProperties\": false\n }\n },\n \"required\": [\n \"id\",\n \"status\",\n \"total\",\n \"results\"\n ],\n \"additionalProperties\": false\n },\n \"MigrationOperationResultEdge\": {\n \"type\": \"object\",\n \"properties\": {\n \"node\": {\n \"$ref\": \"#/definitions/MigrationOperationResultNode\"\n }\n },\n \"required\": [\n \"node\"\n ],\n \"additionalProperties\": false\n },\n \"MigrationOperationResultNode\": {\n \"type\": \"object\",\n \"properties\": {\n \"shopId\": {\n \"type\": \"string\"\n },\n \"code\": {\n \"type\": \"string\",\n \"enum\": [\n \"SCHEDULED\",\n \"CANCELED\",\n \"INVALID_PLAN\",\n \"INELIGIBLE\",\n \"BLOCKED\",\n \"ALREADY_SCHEDULED\",\n \"ALREADY_MIGRATED\",\n \"NOT_FOUND\",\n \"INTERNAL_ERROR\"\n ]\n }\n },\n \"required\": [\n \"shopId\",\n \"code\"\n ],\n \"additionalProperties\": false\n },\n \"MigrationSubmissionFailure\": {\n \"anyOf\": [\n {\n \"type\": \"object\",\n \"properties\": {\n \"type\": {\n \"type\": \"string\",\n \"const\": \"submission\"\n },\n \"batchIndex\": {\n \"type\": \"number\"\n },\n \"userErrors\": {\n \"type\": \"array\",\n \"items\": {\n \"$ref\": \"#/definitions/MigrationUserError\"\n }\n }\n },\n \"required\": [\n \"type\",\n \"batchIndex\",\n \"userErrors\"\n ],\n \"additionalProperties\": false\n },\n {\n \"type\": \"object\",\n \"properties\": {\n \"type\": {\n \"type\": \"string\",\n \"const\": \"operations\"\n },\n \"operationIds\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\"\n }\n }\n },\n \"required\": [\n \"type\",\n \"operationIds\"\n ],\n \"additionalProperties\": false\n }\n ]\n },\n \"MigrationUserError\": {\n \"type\": \"object\",\n \"properties\": {\n \"message\": {\n \"type\": \"string\"\n },\n \"field\": {\n \"anyOf\": [\n {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\"\n }\n },\n {\n \"type\": \"null\"\n }\n ]\n }\n },\n \"required\": [\n \"message\",\n \"field\"\n ],\n \"additionalProperties\": false\n }\n },\n \"$schema\": \"http://json-schema.org/draft-07/schema#\"\n}\n```", "descriptionWithMarkdown": "Schedules manual-billing subscriptions to migrate to Shopify-managed app pricing.\n\nWhen `--input` is omitted, the command reads CSV data from stdin. Use `--input ` to read from a file. `--input -` is also supported as an explicit stdin path.\n\n- Required CSV columns: `shop_id`, `target_plan_handle`, and `price_behavior`.\n- Optional CSV column: `notification`.\n- Example header: `shop_id,target_plan_handle,price_behavior,notification`.\n- Example row: `123456789,pro,HONOR_BILLING_PRICE,WHEN_REQUIRED`.\n\n`price_behavior` must be `HONOR_BILLING_PRICE` or `PLAN_PRICE`. `notification` can be `OPT_OUT` or `WHEN_REQUIRED` and defaults to `WHEN_REQUIRED` when omitted or blank.\n\nValidation is atomic: the command submits no operations unless the entire CSV is valid. Valid rows are submitted in batches of 250 shops. Preserve every operation GID printed by the command so you can check or cancel the submitted operations.\n\nRun the command from an app project. By default, it uses the Client ID from the active app configuration. Use `--path` to select an app directory or `--config` to select a configuration. Pass `--client-id` to select a different app within the project. Use `--reset` to relink the app.\n\nUse `--force` to skip confirmation and immediately submit every valid row. With `--watch`, human-readable output shows accepted identifiers before polling begins, then displays operation progress and the final outcome. With `--json --watch`, the command outputs one structured JSON document after every operation reaches a terminal status.", "examples": [ "<%= config.bin %> <%= command.id %> --input migrations.csv --force", diff --git a/packages/eslint-plugin-cli/rules/json-output-command-exceptions.js b/packages/eslint-plugin-cli/rules/json-output-command-exceptions.js index 163c435672a..5a988b7b78c 100644 --- a/packages/eslint-plugin-cli/rules/json-output-command-exceptions.js +++ b/packages/eslint-plugin-cli/rules/json-output-command-exceptions.js @@ -31,7 +31,6 @@ const commandExceptions = [ 'packages/app/src/cli/commands/app/import/dashboard-extensions.ts', 'packages/app/src/cli/commands/app/init.ts', 'packages/app/src/cli/commands/app/release.ts', - 'packages/app/src/cli/commands/app/subscription-migrations/schedule.ts', 'packages/app/src/cli/commands/app/subscription-migrations/status.ts', 'packages/app/src/cli/commands/app/subscription-migrations/unschedule.ts', 'packages/app/src/cli/commands/app/webhook/trigger.ts',