From 300600d8948af9922b9d0a8fa25adcd8426502c2 Mon Sep 17 00:00:00 2001 From: kiannidev <156195510+kiannidev@users.noreply.github.com> Date: Thu, 2 Jul 2026 09:44:22 +0200 Subject: [PATCH 1/2] fix(enrichment): classify workflow paths case-insensitively in analysis-context The actionPin scheduler gate used case-sensitive workflow path matching, so mixed-case workflow files were skipped even after the analyzer learned to scan them. Co-authored-by: Cursor --- review-enrichment/src/analysis-context.ts | 2 +- .../test/analysis-context.test.ts | 25 +++++++++++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/review-enrichment/src/analysis-context.ts b/review-enrichment/src/analysis-context.ts index 8966bae7c3..3c3571484d 100644 --- a/review-enrichment/src/analysis-context.ts +++ b/review-enrichment/src/analysis-context.ts @@ -422,7 +422,7 @@ function categorizeFile(path: string): FileCategory { ) { return { path, extension, category: "lockfile" }; } - if (path.startsWith(".github/workflows/")) { + if (/^\.github\/workflows\//i.test(path.replace(/\\/g, "/"))) { return { path, extension, category: "workflow" }; } if ( diff --git a/review-enrichment/test/analysis-context.test.ts b/review-enrichment/test/analysis-context.test.ts index 5a68d4073c..eecf789f5b 100644 --- a/review-enrichment/test/analysis-context.test.ts +++ b/review-enrichment/test/analysis-context.test.ts @@ -420,3 +420,28 @@ test("createAnalysisContext treats capped patch scans as added-line presence", ( true, ); }); + +test("createAnalysisContext classifies workflow paths case-insensitively", () => { + const context = createAnalysisContext({ + repoFullName: "JSONbored/gittensory", + prNumber: 2516, + files: [ + { + path: ".github/Workflows/CI.YML", + patch: "@@ -1,0 +1,1 @@\n+ - uses: pnpm/action-setup@v3", + }, + { + path: "docs/readme.md", + patch: "@@ -1,0 +1,1 @@\n+# docs", + }, + ], + }); + + assert.deepEqual( + context.fileCategories.map((file) => [file.path, file.category]), + [ + [".github/Workflows/CI.YML", "workflow"], + ["docs/readme.md", "docs"], + ], + ); +}); From c299dd2e9624af1e371c375347355d475b85cb99 Mon Sep 17 00:00:00 2001 From: kiannidev <156195510+kiannidev@users.noreply.github.com> Date: Thu, 2 Jul 2026 09:54:27 +0200 Subject: [PATCH 2/2] fix(enrichment): share workflow path matcher across scheduler and analyzer Extract isWorkflowPath so analysis-context categorization and actionPin use the same case-insensitive rule, with a scheduler regression test. Co-authored-by: Cursor --- review-enrichment/src/analysis-context.ts | 3 ++- .../src/analyzers/actions-pin.ts | 6 +---- review-enrichment/src/workflow-path.ts | 6 +++++ review-enrichment/test/scheduler.test.ts | 27 +++++++++++++++++++ 4 files changed, 36 insertions(+), 6 deletions(-) create mode 100644 review-enrichment/src/workflow-path.ts diff --git a/review-enrichment/src/analysis-context.ts b/review-enrichment/src/analysis-context.ts index 3c3571484d..08140e3922 100644 --- a/review-enrichment/src/analysis-context.ts +++ b/review-enrichment/src/analysis-context.ts @@ -17,6 +17,7 @@ import { type BoundedFetchOptions, type BoundedFetchResult, } from "./external-fetch.js"; +import { isWorkflowPath } from "./workflow-path.js"; type ChangedFile = NonNullable[number]; @@ -422,7 +423,7 @@ function categorizeFile(path: string): FileCategory { ) { return { path, extension, category: "lockfile" }; } - if (/^\.github\/workflows\//i.test(path.replace(/\\/g, "/"))) { + if (isWorkflowPath(path)) { return { path, extension, category: "workflow" }; } if ( diff --git a/review-enrichment/src/analyzers/actions-pin.ts b/review-enrichment/src/analyzers/actions-pin.ts index d6a7324883..4a806f21da 100644 --- a/review-enrichment/src/analyzers/actions-pin.ts +++ b/review-enrichment/src/analyzers/actions-pin.ts @@ -3,15 +3,11 @@ // where a compromised upstream tag silently re-points and runs in your CI with your secrets. Pure compute, no network. // Official actions/* + github/* are excluded (lowest risk, extremely common) to keep the signal high. Line-cited. import type { EnrichRequest, ActionPinFinding } from "../types.js"; +import { isWorkflowPath } from "../workflow-path.js"; const USES_RE = /^\s*-?\s*["']?uses["']?\s*:\s*["']?([\w.-]+\/[\w./-]+)@([^\s"'#]+)/; const FULL_SHA = /^[0-9a-f]{40}$/; const OFFICIAL = /^(actions|github)\//; -const WORKFLOW_PATH = /^\.github\/workflows\/.+\.ya?ml$/; - -function isWorkflowPath(path: string): boolean { - return WORKFLOW_PATH.test(path.replace(/\\/g, "/").toLowerCase()); -} /** Scan one workflow patch's added lines for unpinned third-party `uses:` refs, line-cited via hunk headers. Pure. */ export function scanWorkflowPins( diff --git a/review-enrichment/src/workflow-path.ts b/review-enrichment/src/workflow-path.ts new file mode 100644 index 0000000000..9934887505 --- /dev/null +++ b/review-enrichment/src/workflow-path.ts @@ -0,0 +1,6 @@ +const WORKFLOW_PATH = /^\.github\/workflows\/.+\.ya?ml$/; + +/** GitHub workflow paths are case-insensitive; normalize separators before matching. */ +export function isWorkflowPath(path: string): boolean { + return WORKFLOW_PATH.test(path.replace(/\\/g, "/").toLowerCase()); +} diff --git a/review-enrichment/test/scheduler.test.ts b/review-enrichment/test/scheduler.test.ts index 512da0b2f6..635aadc8d9 100644 --- a/review-enrichment/test/scheduler.test.ts +++ b/review-enrichment/test/scheduler.test.ts @@ -246,3 +246,30 @@ test("added-line analyzers run when patch scan is capped before additions", asyn assert.equal(brief.telemetry.skippedWorkByCategory.analyzer_no_added_lines, undefined); assert.ok(brief.telemetry.cappedWorkByCategory.has_added_lines_patch_bytes > 0); }); + +test("actionPin runs for mixed-case workflow paths", async () => { + let actionPinRan = false; + const brief = await buildBrief( + { + repoFullName: "JSONbored/gittensory", + prNumber: 2516, + analyzers: ["actionPin"], + files: [ + { + path: ".github/Workflows/CI.YML", + patch: "@@ -1,0 +5,1 @@\n+ - uses: pnpm/action-setup@v3", + }, + ], + }, + { + actionPin: async () => { + actionPinRan = true; + return [{ file: ".github/Workflows/CI.YML", line: 5, action: "pnpm/action-setup", ref: "v3" }]; + }, + }, + ); + + assert.equal(actionPinRan, true); + assert.equal(brief.analyzerStatus.actionPin, "ok"); + assert.notEqual(brief.telemetry.analyzers.actionPin.skipReason, "no_workflow"); +});