Skip to content

Add typed JSON output to app bulk execute - #8783

Merged
isaacroldan merged 5 commits into
codex/app-bulk-cancel-jsonfrom
codex/app-bulk-execute-json
Oct 8, 2026
Merged

isaacroldan merged 5 commits into
codex/app-bulk-cancel-jsonfrom
codex/app-bulk-execute-json

Conversation

@isaacroldan

@isaacroldan isaacroldan commented Oct 6, 2026 •

Copy link
Copy Markdown
Contributor

WHY are these changes introduced?

Related to https://github.com/shop/issues-develop/issues/23664

Expose a finite, typed JSON result for bulk execution while keeping the existing raw JSONL and file formats available.

WHAT is this pull request doing?

Add JSON output for app bulk execute on top of the earlier stack layers. Separate preparation, execution, and final presentation. Reuse the shared operation projection and diagnostic events.

Unwatched execution returns operation metadata. Watched JSON output includes native data in resultsJsonl; file output emits an absolute path/format receipt. Preserve the existing text-mode output and exact JSONL file contents. Treat query records as data and completed zero-match queries as successful empty exports. Detect mutation row errors through the selected userErrors fields, including aliases and fragments, so watched JSON execution exits nonzero when a row fails.

Representative text output:

Bulk operation is running.
Monitor its progress with:
shopify app bulk status --id=123

Matching JSON output:

{
  "storeDomain": "shop.myshopify.com",
  "apiVersion": "2026-01",
  "status": "success",
  "operation": {
    "gid": "gid://shopify/BulkOperation/123",
    "type": "QUERY",
    "status": "RUNNING",
    "errorCode": null,
    "createdAt": "2026-09-01T00:00:00Z",
    "completedAt": null,
    "objectCount": "2",
    "url": null,
    "partialDataUrl": null
  }
}

How to manually test your changes?

Use a linked app installed on a development store.

  1. Run pnpm shopify app bulk execute --store <store.myshopify.com> --query 'query { products { edges { node { id } } } }', then repeat with --json.
  2. Add --watch. Without JSON mode, confirm stdout still contains raw JSONL. With JSON mode, confirm one result object contains resultsJsonl.
  3. Add --output-file ./results.jsonl to the watched JSON command. Confirm stdout contains only an absolute JSONL file receipt and the file contains native JSONL.
  4. Repeat the watched JSON file command with a query that matches no products, such as query { products(query: "id:0") { edges { node { id } } } }. Confirm exit 0, an empty file, and a file receipt.
  5. Select id errors: tags for products with tags. Confirm watched JSON output reports success rather than treating tags as errors.
  6. Run pnpm shopify app bulk execute --json-schema.

Checklist

  • I've considered possible cross-platform impacts (Mac, Linux, Windows)
  • I've considered possible documentation changes
  • I've considered analytics changes to measure impact
  • The change is user-facing; a minor changeset is included.

@isaacroldan
isaacroldan requested review from a team as code owners October 6, 2026 08:29
@github-actions github-actions Bot added shopify.dev preview Area: @shopify/cli @shopify/cli package issues labels Oct 6, 2026
@isaacroldan
isaacroldan added this pull request to stack #8784 October 6, 2026 08:30
@isaacroldan

Copy link
Copy Markdown
Contributor Author

/snapit

@github-actions

github-actions Bot commented Oct 6, 2026

Copy link
Copy Markdown
Contributor

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

Built from d7aaae183d8f2aff3a7cd0bb625eff770b7b6397. Workflow run.

Test the snapshot by installing your package globally:

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

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.

@isaacroldan
isaacroldan force-pushed the codex/app-bulk-execute-json branch from d7aaae1 to 2a27ac7 Compare October 6, 2026 10:00
@isaacroldan
isaacroldan removed this pull request from stack #8784 October 6, 2026 10:03
@isaacroldan
isaacroldan added this pull request to stack #8789 October 6, 2026 10:03
@isaacroldan

Copy link
Copy Markdown
Contributor Author

/snapit

@github-actions

github-actions Bot commented Oct 6, 2026

Copy link
Copy Markdown
Contributor

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

Built from 2a27ac73f13ccd02dfcb8498619ea9ca59d82af6. Workflow run.

Test the snapshot by installing your package globally:

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

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.

@isaacroldan
isaacroldan force-pushed the codex/app-bulk-execute-json branch 2 times, most recently from 2816253 to 58364d0 Compare October 7, 2026 15:33

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

This is working overall in my tests + code changes LGTM. Agent smoke-test found an edge case that's worth double-checking.

}
if (!operation) throw new BugError('Bulk operation response returned no operation.')

const partial = operation.type === 'MUTATION' && results !== undefined && resultsContainUserErrors(results)

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.

[P2] Detect aliased mutation errors before reporting success

A mutation can select errors: userErrors { message }. A completed bulk operation can then contain a failed row such as:

{"data":{"productUpdate":{"errors":[{"message":"Rejected"}]}}}

resultsContainUserErrors() checks only the literal nested key userErrors. For this row, partial is false:

  • With --watch --json, the command reports status: "success" instead of "partial".
  • With --watch --json --output-file, it writes the failed row and emits the file receipt.
  • Both paths leave process.exitCode unset (normal exit 0), instead of setting it to 1.

I reproduced both cases through the command, service, schema, and output writer. The tests use real temporary files and simulated external authentication/API responses. No live store operation was run. The same tests pass with non-aliased userErrors and correctly set exit code 1.

Please use the submitted mutation's selection to identify error-response keys, including aliases. Add regression tests for inline and file output, and preserve the native JSONL bytes. Do not treat every field named errors as an error: query data can use that name.

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.

Fixed in af72281470. The JSON result now uses the submitted mutation selection to find userErrors response keys, including field/root aliases and fragments.

Inline output reports partial and exits 1. File output keeps the exact JSONL bytes and emits its receipt with exit 1. Ordinary fields aliased as errors or userErrors are not counted as mutation errors.

Added command regressions for inline/file output and fragments. All four fail with the old check restored. All 149 bulk tests, app build/type checks, lint, and Knip pass. No live store operation was run.


🤖 Generated by the review-pr skill

@github-actions

github-actions Bot commented Oct 7, 2026

Copy link
Copy Markdown
Contributor

Differences in type declarations

We detected differences in the type declarations generated by Typescript for this branch compared to the baseline ('main' branch). Please, review them to ensure they are backward-compatible. Here are some important things to keep in mind:

  • Some seemingly private modules might be re-exported through public modules.
  • If the branch is behind main you might see odd diffs, rebase main into this branch.

New type declarations

We found no new type declarations in this PR

Existing type declarations

packages/cli-kit/dist/public/node/system.d.ts
@@ -104,9 +104,9 @@ export declare function sleep(seconds: number): Promise<void>;
  */
 export declare function terminalSupportsHyperlinks(): boolean;
 /**
- * Check if standard input and standard error are terminals that support prompting.
+ * Check if the standard input and output streams support prompting.
  *
- * @returns True if standard input and standard error support prompting.
+ * @returns True if the standard input and output streams support prompting.
  */
 export declare function terminalSupportsPrompting(): boolean;
 /**
packages/cli-kit/dist/public/node/ui.d.ts
@@ -21,7 +21,6 @@ interface UIDebugOptions {
     skipTTYCheck?: boolean;
 }
 export interface RenderConcurrentOptions extends PartialBy<ConcurrentOutputProps, 'abortSignal'> {
-    /** Ink options for terminal UI. Finite JSON output uses the command event channel on stderr instead. */
     renderOptions?: RenderOptions;
 }
 /**
packages/cli-kit/dist/private/node/ui/components/ConcurrentOutput.d.ts
@@ -1,4 +1,4 @@
-import { type OutputProcess } from '../../../../public/node/output.js';
+import { OutputProcess } from '../../../../public/node/output.js';
 import { AbortSignal } from '../../../../public/node/abort.js';
 import { FunctionComponent } from 'react';
 export interface ConcurrentOutputProps {
@@ -6,21 +6,14 @@ export interface ConcurrentOutputProps {
     prefixColumnSize?: number;
     abortSignal: AbortSignal;
     showTimestamps?: boolean;
-    /**
-     * Keeps terminal UI running after all processes finish. Defaults to false.
-     * In JSON mode, false uses finite progress/diagnostic events; true retains streaming terminal UI.
-     */
     keepRunningAfterProcessesResolve?: boolean;
     useAlternativeColorPalette?: boolean;
 }
 interface ConcurrentOutputContext {
     outputPrefix?: string;
-    /** Controls ANSI stripping for terminal output. JSON diagnostics are always unstyled. */
     stripAnsi?: boolean;
 }
 declare function useConcurrentOutputContext<T>(context: ConcurrentOutputContext, callback: () => T): T;
-/** Runs finite processes concurrently and routes their output through the shared diagnostic context. */
-export declare function runConcurrentProcessesForJson({ processes, abortSignal, }: Pick<ConcurrentOutputProps, 'processes' | 'abortSignal'>): Promise<void>;
 /**
  * Renders output from concurrent processes to the terminal.
  * Output will be divided in a three column layout

@isaacroldan
isaacroldan force-pushed the codex/app-bulk-execute-json branch from af72281 to 60eeaeb Compare October 8, 2026 08:13
@isaacroldan
isaacroldan added this pull request to the merge queue Oct 8, 2026
Merged via the queue into main with commit f51b5cc Oct 8, 2026
30 of 31 checks passed
@isaacroldan
isaacroldan deleted the codex/app-bulk-execute-json branch October 8, 2026 13:59
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.

2 participants