diff --git a/packages/app/src/cli/commands/app/bulk/status.ts b/packages/app/src/cli/commands/app/bulk/status.ts index 5923e1e3b1..4b2e28951a 100644 --- a/packages/app/src/cli/commands/app/bulk/status.ts +++ b/packages/app/src/cli/commands/app/bulk/status.ts @@ -1,7 +1,11 @@ import {appFlags} from '../../../flags.js' import AppLinkedCommand, {AppLinkedCommandOutput} from '../../../utilities/app-linked-command.js' import {prepareAppStoreContext} from '../../../utilities/execute-command-helpers.js' -import {getBulkOperationStatus, listBulkOperations} from '../../../services/bulk-operations/bulk-operation-status.js' +import { + getBulkOperationStatus, + listBulkOperations, + normalizeBulkOperationId, +} from '../../../services/bulk-operations/bulk-operation-status.js' import {Flags} from '@oclif/core' import {globalFlags} from '@shopify/cli-kit/node/cli' import {normalizeStoreFqdn} from '@shopify/cli-kit/node/context/fqdn' @@ -18,7 +22,8 @@ export default class BulkStatus extends AppLinkedCommand { ...globalFlags, ...appFlags, id: Flags.string({ - description: 'The bulk operation ID. If not provided, lists all bulk operations in the last 7 days.', + description: + 'The bulk operation ID (numeric ID or full GID). If not provided, lists all bulk operations in the last 7 days.', env: 'SHOPIFY_FLAG_ID', }), store: Flags.string({ @@ -38,7 +43,7 @@ export default class BulkStatus extends AppLinkedCommand { await getBulkOperationStatus({ organization: appContextResult.organization, storeFqdn: store.shopDomain, - operationId: flags.id, + operationId: normalizeBulkOperationId(flags.id), remoteApp: appContextResult.remoteApp, }) } else { diff --git a/packages/app/src/cli/services/bulk-operations/bulk-operation-status.test.ts b/packages/app/src/cli/services/bulk-operations/bulk-operation-status.test.ts index 2daf2a8196..fb5bb6f364 100644 --- a/packages/app/src/cli/services/bulk-operations/bulk-operation-status.test.ts +++ b/packages/app/src/cli/services/bulk-operations/bulk-operation-status.test.ts @@ -1,4 +1,9 @@ -import {getBulkOperationStatus, listBulkOperations} from './bulk-operation-status.js' +import { + getBulkOperationStatus, + listBulkOperations, + normalizeBulkOperationId, + extractBulkOperationId, +} from './bulk-operation-status.js' import {GetBulkOperationByIdQuery} from '../../api/graphql/bulk-operations/generated/get-bulk-operation-by-id.js' import {OrganizationApp, Organization, OrganizationSource} from '../../models/organization.js' import {ListBulkOperationsQuery} from '../../api/graphql/bulk-operations/generated/list-bulk-operations.js' @@ -36,6 +41,37 @@ afterEach(() => { mockAndCaptureOutput().clear() }) +describe('normalizeBulkOperationId', () => { + test('returns GID as-is when already in GID format', () => { + const gid = 'gid://shopify/BulkOperation/123' + expect(normalizeBulkOperationId(gid)).toBe(gid) + }) + + test('converts numeric ID to GID format', () => { + expect(normalizeBulkOperationId('123')).toBe('gid://shopify/BulkOperation/123') + expect(normalizeBulkOperationId('456789')).toBe('gid://shopify/BulkOperation/456789') + }) + + test('returns non-numeric, non-GID string as-is', () => { + const invalidId = 'invalid-id' + expect(normalizeBulkOperationId(invalidId)).toBe(invalidId) + }) +}) + +describe('extractBulkOperationId', () => { + test('extracts numeric ID from GID', () => { + expect(extractBulkOperationId('gid://shopify/BulkOperation/123')).toBe('123') + expect(extractBulkOperationId('gid://shopify/BulkOperation/456789')).toBe('456789') + }) + + test('returns input as-is if not a valid GID format', () => { + expect(extractBulkOperationId('gid://shopify/BulkOperation/ABC')).toBe('gid://shopify/BulkOperation/ABC') + expect(extractBulkOperationId('BulkOperation/123')).toBe('BulkOperation/123') + expect(extractBulkOperationId('invalid-id')).toBe('invalid-id') + expect(extractBulkOperationId('123')).toBe('123') + }) +}) + describe('getBulkOperationStatus', () => { function mockBulkOperation( overrides?: Partial>, @@ -222,15 +258,15 @@ describe('listBulkOperations', () => { │ │ ╰──────────────────────────────────────────────────────────────────────────────╯ - ID STATUS COU DATE CREATED DATE RESULTS - T FINISHED + I STATUS COUNT DATE CREATED DATE FINISHED RESULTS - ──────────────── ────── ─── ──────────── ─────────── ─────────────────────────── - ──────────── ── ── ─────── ─────── ─────────────────── - gid://shopify/Bu COMPLE 123 2025-11-10 2025-11-10 download ( https://example. - kOperation/1 ED 5K 12:37:52 16:37:12 com/results.jsonl ) - gid://shopify/Bu RUNNIN 100 2025-11-11 - kOperation/2 15:37:52" + ─ ─────── ───── ────────────── ────────────── ────────────────────────────────── + ─ ──── ──── ──────────── + 1 COMPLET 123.5 2025-11-10 2025-11-10 download ( + D 12:37:52 16:37:12 https://example.com/results.jsonl + ) + 2 RUNNING 100 2025-11-11 + 15:37:52" `) }) diff --git a/packages/app/src/cli/services/bulk-operations/bulk-operation-status.ts b/packages/app/src/cli/services/bulk-operations/bulk-operation-status.ts index d404d95092..1dca3cabc1 100644 --- a/packages/app/src/cli/services/bulk-operations/bulk-operation-status.ts +++ b/packages/app/src/cli/services/bulk-operations/bulk-operation-status.ts @@ -21,6 +21,27 @@ import colors from '@shopify/cli-kit/node/colors' const API_VERSION = '2026-01' +export function normalizeBulkOperationId(id: string): string { + // If already a GID, return as-is + if (id.startsWith('gid://')) { + return id + } + + // If numeric, convert to GID + if (/^\d+$/.test(id)) { + return `gid://shopify/BulkOperation/${id}` + } + + // Otherwise return as-is (let API handle any errors) + return id +} + +export function extractBulkOperationId(gid: string): string { + // Extract the numeric ID from a GID like "gid://shopify/BulkOperation/123" + const match = gid.match(/^gid:\/\/shopify\/BulkOperation\/(\d+)$/) + return match?.[1] ?? gid +} + interface GetBulkOperationStatusOptions { organization: Organization storeFqdn: string @@ -104,7 +125,7 @@ export async function listBulkOperations(options: ListBulkOperationsOptions): Pr }) const operations = response.bulkOperations.nodes.map((operation) => ({ - id: operation.id, + id: extractBulkOperationId(operation.id), status: formatStatus(operation.status), count: formatCount(operation.objectCount as number), dateCreated: formatDate(new Date(String(operation.createdAt))), diff --git a/packages/app/src/cli/services/bulk-operations/execute-bulk-operation.test.ts b/packages/app/src/cli/services/bulk-operations/execute-bulk-operation.test.ts index 7769b4023c..253bb61494 100644 --- a/packages/app/src/cli/services/bulk-operations/execute-bulk-operation.test.ts +++ b/packages/app/src/cli/services/bulk-operations/execute-bulk-operation.test.ts @@ -365,7 +365,7 @@ describe('executeBulkOperation', () => { expect(renderInfo).toHaveBeenCalledWith({ headline: `Bulk operation ${createdBulkOperation.id} is still running in the background.`, - body: ['Monitor its progress with:', {command: expect.stringContaining('shopify app bulk status')}], + body: ['Monitor its progress with:\n', {command: expect.stringContaining('shopify app bulk status')}], }) expect(downloadBulkOperationResults).not.toHaveBeenCalled() }) diff --git a/packages/app/src/cli/services/bulk-operations/execute-bulk-operation.ts b/packages/app/src/cli/services/bulk-operations/execute-bulk-operation.ts index 8895d030ed..b140c2ae52 100644 --- a/packages/app/src/cli/services/bulk-operations/execute-bulk-operation.ts +++ b/packages/app/src/cli/services/bulk-operations/execute-bulk-operation.ts @@ -3,6 +3,7 @@ import {runBulkOperationMutation} from './run-mutation.js' import {watchBulkOperation, type BulkOperation} from './watch-bulk-operation.js' import {formatBulkOperationStatus} from './format-bulk-operation-status.js' import {downloadBulkOperationResults} from './download-bulk-operation-results.js' +import {extractBulkOperationId} from './bulk-operation-status.js' import { createAdminSessionAsApp, validateSingleOperation, @@ -169,7 +170,10 @@ function validateGraphQLDocument(graphqlOperation: string, variablesJsonl?: stri } function statusCommandHelpMessage(operationId: string): TokenItem { - return ['Monitor its progress with:', {command: `shopify app bulk status --id="${operationId}}"`}] + return [ + 'Monitor its progress with:\n', + {command: `shopify app bulk status --id=${extractBulkOperationId(operationId)}`}, + ] } function isMutation(graphqlOperation: string): boolean { diff --git a/packages/cli/oclif.manifest.json b/packages/cli/oclif.manifest.json index 160ddd015c..809e0e4ccf 100644 --- a/packages/cli/oclif.manifest.json +++ b/packages/cli/oclif.manifest.json @@ -263,7 +263,7 @@ "type": "option" }, "id": { - "description": "The bulk operation ID. If not provided, lists all bulk operations in the last 7 days.", + "description": "The bulk operation ID (numeric ID or full GID). If not provided, lists all bulk operations in the last 7 days.", "env": "SHOPIFY_FLAG_ID", "hasDynamicHelp": false, "multiple": false,