Skip to content

Add --ignore to app security check and instructions - #8702

Merged
jek merged 7 commits into
app-security/exclude-gitignorefrom
app-security/path-filter
Sep 30, 2026
Merged

jek merged 7 commits into
app-security/exclude-gitignorefrom
app-security/path-filter

Conversation

@jek

@jek jek commented Sep 29, 2026 •

Copy link
Copy Markdown
Contributor

Stacked on #8691.

WHY are these changes introduced?

The files shopify app security check scans are decided entirely by the default exclusions and .gitignore (#8691). Users can't skip tracked generated code, or scan a folder the defaults exclude, such as build/ or test/.

WHAT is this pull request doing?

Adds a repeatable --ignore flag to app security check. Each value is one .gitignore line relative to the app directory, and a leading ! includes files again.

  • Patterns take precedence over the default and git exclusions, and later patterns take precedence over earlier ones. As in git, a file can't be included again while its parent folder is ignored. A folder git ignores as a whole can only be included again as a folder (!logs/, not !logs/debug.log).
  • The generated compile and clean commands repeat the patterns, so compiling scans the same files. Patterns aren't recorded in the trace or submission. If compiling with different patterns changes the scanned files, the input hash no longer matches the findings' source_scan_id and they're rejected; the error says to reuse the scan's --ignore and --config values.
  • app security instructions doesn't take --ignore. It would validate patterns against its own flags, and accept values such as --clean that the check commands it generates then read as flags. The instructions check offers after a scan repeat its patterns.
  • Dependabot and Renovate config follows the patterns: an excluded config file counts as missing.
  • Values that would silently do nothing or crash the scan are rejected when the flag is parsed.
  • --ignore has no environment variable: oclif passes a repeatable flag's variable as one string, so it could only hold one pattern.

Patterns are matched with the ignore package, as elsewhere in the CLI. It differs from git on some unusual syntax, such as an escaped backslash before a regex special character.

How to manually test your changes?

In a git-tracked app, with a Shopify token-shaped value (shpat_ followed by 32 hex characters):

  1. Put the value in generated/client.ts, git add it, and run pnpm shopify app security check --path /path/to/app: it's reported.
  2. pnpm shopify app security check --path /path/to/app --ignore 'generated/': it's no longer reported.
  3. Put the value in build/config.js and run pnpm shopify app security check --path /path/to/app --ignore '!build/': it's reported, although build/ is excluded by default.
  4. pnpm shopify app security check --path /path/to/app --ignore 'src/[id/x.ts': the flag is rejected with a message.

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 — I've identified the correct bump type (patch for bug fixes · minor for new features · major for breaking changes) and added a changeset with pnpm changeset add

@jek
jek requested review from a team as code owners September 29, 2026 20:26
@jek
jek added this pull request to stack #8703 September 29, 2026 20:26
@github-actions github-actions Bot added the Area: @shopify/app @shopify/app package issues label Sep 29, 2026
@github-actions

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/common/command-events.d.ts
@@ -23,14 +23,14 @@ export declare const commandDiagnosticEventSchema: z.ZodObject<{
 export declare const commandProgressEventSchema: z.ZodObject<{
     type: z.ZodLiteral<"progress">;
     timestamp: z.ZodString;
-    status: z.ZodEnum<["started", "updated", "retrying", "completed", "failed"]>;
+    status: z.ZodEnum<["started", "updated", "completed"]>;
     operation: z.ZodString;
     message: z.ZodOptional<z.ZodString>;
     current: z.ZodOptional<z.ZodNumber>;
     total: z.ZodOptional<z.ZodNumber>;
 }, "strict", z.ZodTypeAny, {
     type: "progress";
-    status: "started" | "updated" | "retrying" | "completed" | "failed";
+    status: "started" | "updated" | "completed";
     timestamp: string;
     operation: string;
     message?: string | undefined;
@@ -38,7 +38,7 @@ export declare const commandProgressEventSchema: z.ZodObject<{
     total?: number | undefined;
 }, {
     type: "progress";
-    status: "started" | "updated" | "retrying" | "completed" | "failed";
+    status: "started" | "updated" | "completed";
     timestamp: string;
     operation: string;
     message?: string | undefined;
@@ -67,14 +67,14 @@ export declare const commandEventSchema: z.ZodDiscriminatedUnion<"type", [z.ZodO
 }>, z.ZodObject<{
     type: z.ZodLiteral<"progress">;
     timestamp: z.ZodString;
-    status: z.ZodEnum<["started", "updated", "retrying", "completed", "failed"]>;
+    status: z.ZodEnum<["started", "updated", "completed"]>;
     operation: z.ZodString;
     message: z.ZodOptional<z.ZodString>;
     current: z.ZodOptional<z.ZodNumber>;
     total: z.ZodOptional<z.ZodNumber>;
 }, "strict", z.ZodTypeAny, {
     type: "progress";
-    status: "started" | "updated" | "retrying" | "completed" | "failed";
+    status: "started" | "updated" | "completed";
     timestamp: string;
     operation: string;
     message?: string | undefined;
@@ -82,7 +82,7 @@ export declare const commandEventSchema: z.ZodDiscriminatedUnion<"type", [z.ZodO
     total?: number | undefined;
 }, {
     type: "progress";
-    status: "started" | "updated" | "retrying" | "completed" | "failed";
+    status: "started" | "updated" | "completed";
     timestamp: string;
     operation: string;
     message?: string | undefined;
packages/cli-kit/dist/public/node/command-events.d.ts
@@ -29,14 +29,14 @@ export declare const commandEventOutputSchema: import("./json-output-schema.js")
 }>, import("zod").ZodObject<{
     type: import("zod").ZodLiteral<"progress">;
     timestamp: import("zod").ZodString;
-    status: import("zod").ZodEnum<["started", "updated", "retrying", "completed", "failed"]>;
+    status: import("zod").ZodEnum<["started", "updated", "completed"]>;
     operation: import("zod").ZodString;
     message: import("zod").ZodOptional<import("zod").ZodString>;
     current: import("zod").ZodOptional<import("zod").ZodNumber>;
     total: import("zod").ZodOptional<import("zod").ZodNumber>;
 }, "strict", import("zod").ZodTypeAny, {
     type: "progress";
-    status: "started" | "updated" | "retrying" | "completed" | "failed";
+    status: "started" | "updated" | "completed";
     timestamp: string;
     operation: string;
     message?: string | undefined;
@@ -44,7 +44,7 @@ export declare const commandEventOutputSchema: import("./json-output-schema.js")
     total?: number | undefined;
 }, {
     type: "progress";
-    status: "started" | "updated" | "retrying" | "completed" | "failed";
+    status: "started" | "updated" | "completed";
     timestamp: string;
     operation: string;
     message?: string | undefined;
packages/cli-kit/dist/public/node/ui.d.ts
@@ -318,8 +318,6 @@ export declare function renderTasks<TContext>(tasks: Task<TContext>[], { renderO
 export interface RenderSingleTaskOptions<T> {
     title: TokenizedString;
     task: (updateStatus: (status: TokenizedString) => void) => Promise<T>;
-    /** The number of additional attempts after a failure. Defaults to zero. */
-    retry?: number;
     onAbort?: () => void;
     renderOptions?: RenderOptions;
 }
@@ -328,13 +326,12 @@ export interface RenderSingleTaskOptions<T> {
  * @param options - Configuration object
  * @param options.title - The initial title to display with the loading bar
  * @param options.task - The async task to execute. Receives an updateStatus callback to change the displayed title.
- * @param options.retry - The number of additional attempts after a failure. Defaults to zero.
  * @param options.renderOptions - Optional render configuration
  * @returns The result of the task
  * @example
  * Loading app ...
  */
-export declare function renderSingleTask<T>({ title, task, retry, onAbort, renderOptions, }: RenderSingleTaskOptions<T>): Promise<T>;
+export declare function renderSingleTask<T>({ title, task, onAbort, renderOptions, }: RenderSingleTaskOptions<T>): Promise<T>;
 export interface RenderTextPromptOptions extends Omit<TextPromptProps, 'onSubmit'> {
     renderOptions?: RenderOptions;
 }
packages/cli-kit/dist/private/node/ui/hooks/use-async-and-unmount.d.ts
@@ -1,6 +1,6 @@
-interface Options<T> {
-    onFulfilled?: (result: T) => unknown;
+interface Options {
+    onFulfilled?: () => unknown;
     onRejected?: (error: Error) => void;
 }
-export default function useAsyncAndUnmount<T>(asyncFunction: () => Promise<T>, { onFulfilled, onRejected }?: Options<T>): void;
+export default function useAsyncAndUnmount(asyncFunction: () => Promise<unknown>, { onFulfilled, onRejected }?: Options): void;
 export {};
\ No newline at end of file
packages/cli-kit/dist/private/node/ui/components/Tasks.d.ts
@@ -1,7 +1,14 @@
 import { AbortSignal } from '../../../../public/node/abort.js';
-import { Task } from '../tasks.js';
+import { TokenizedString } from '../../../../public/node/output.js';
 import React from 'react';
-export type { Task } from '../tasks.js';
+export interface Task<TContext = unknown> {
+    title: string | TokenizedString;
+    task: (ctx: TContext, task: Task<TContext>) => Promise<void | Task<TContext>[]>;
+    retry?: number;
+    retryCount?: number;
+    errors?: Error[];
+    skip?: (ctx: TContext) => boolean;
+}
 interface TasksProps<TContext> {
     tasks: Task<TContext>[];
     silent?: boolean;

@jek
jek force-pushed the app-security/path-filter branch 2 times, most recently from 94b1cc0 to cd3543d Compare September 29, 2026 22:39
@jek
jek requested a review from jplhomer September 29, 2026 22:50
Comment thread .changeset/app-security-path-filter.md Outdated
Comment thread packages/app/src/cli/commands/app/security/flags.ts Outdated
Comment thread packages/app/src/cli/services/app-security-engine/run.ts Outdated
jek added a commit that referenced this pull request Sep 30, 2026
A nested repository is excluded like any ignored folder when the app's
repository ignores it, directly or through a parent folder. Finding
repositories below ignored parents would mean walking the ignored trees
that pruning skips; --ignore from #8702 can opt one back in.
jek added 3 commits September 30, 2026 14:09
Each --ignore value is one .gitignore line, relative to the app
directory. Patterns take precedence over the default and gitignore
exclusions: they can ignore more files or, with a leading !, include
ignored ones again. Later patterns take precedence over earlier ones. A
file can't be included again while its parent folder is ignored, and a
folder git ignores as a whole can only be included again as a folder.
Patterns apply to dependency automation configuration too.

When any pattern includes a path again, git's listing also covers the
default directories, so .gitignore still applies inside a default
folder that a pattern includes again.

The generated compile and clean commands repeat the patterns, and app
security instructions accepts them too, so compiling findings re-scans
the same files. A findings file compiled with different patterns is
rejected with a hint to reuse the scan's patterns.

--ignore has no environment variable. oclif reads a repeatable flag's
variable only when the command line has no value for that flag, and
passes it as a single string, so it could hold one pattern and any
--ignore on the command line would silently replace it.

Flag values in the generated commands are always quoted, so a pattern
such as -*.log or check isn't mistaken for a flag or a command word.

Values that would silently do nothing or crash the scan are rejected:
blank values, # comments, a lone !, an odd number of trailing
backslashes, values that span more than one line, patterns with a ..
path segment, which can never match because scanned paths are relative
to the app directory, and any pattern the matcher can't compile, such as
an unclosed [ followed by /.
App Security stays out of public release notes until it ships (#8698).
@jek
jek force-pushed the app-security/path-filter branch from ff1fa1c to 5606f93 Compare September 30, 2026 21:12
@github-actions github-actions Bot added no-changelog This PR doesn't include a changeset entry. Is an internal only change not relevant to end users. and removed Area: @shopify/app @shopify/app package issues labels Sep 30, 2026
jek added 4 commits September 30, 2026 14:15
instructions validated patterns with its own flags, so it accepted values
such as --clean that the check commands it generates then read as flags.
Nothing passes --ignore to instructions. The instructions check offers
after a scan are built from check's own parsed flags, so they still repeat
its patterns and always parse.
Only app-security-commands.ts uses it, and Knip rejects the unused export.
Different --ignore patterns change the hash only when they change which
files are scanned.
…ed config

--ignore '!inner/' re-includes a nested repository that the app's
repository ignores. A pattern that excludes the selected app configuration
doesn't stop it from being scanned for secrets.
@jek
jek added this pull request to the merge queue Sep 30, 2026
Merged via the queue into main with commit df8053d Sep 30, 2026
30 checks passed
@jek
jek deleted the app-security/path-filter branch September 30, 2026 23:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

no-changelog This PR doesn't include a changeset entry. Is an internal only change not relevant to end users.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants