From 4ef142129c790ce490391e0a6516b2babd844860 Mon Sep 17 00:00:00 2001 From: Krishan Kant Sharma Date: Tue, 30 Jun 2026 22:53:42 -0500 Subject: [PATCH] =?UTF-8?q?fix:=20P2=20quick=20wins=20=E2=80=94=20version?= =?UTF-8?q?=20pinning,=20provider=20guard,=20micromatch=20glob?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - action.yml: add `version` input (default "latest"); route through FLAGLINT_VERSION env var so npx flaglint@"$FLAGLINT_VERSION" works safely without shell injection risk - src/config.ts: replace z.enum() with z.string() + runtime assertSupportedProvider() that throws a clear exit-2 message for any provider other than "launchdarkly"; Zod still validates other fields - src/validator/index.ts: replace 30-line hand-rolled glob implementation in matchesBootstrapPattern() with micromatch.isMatch() — micromatch is already a direct dependency (used in migrator/apply.ts) - src/config/tests/config.test.ts: update two tests that previously expected provider:"unleash" to pass through; they now assert the new unsupported-provider error message Signed-off-by: Krishan Kant Sharma --- action.yml | 9 ++++++-- src/config.ts | 21 +++++++++++++----- src/config/tests/config.test.ts | 8 +++---- src/validator/index.ts | 39 +++------------------------------ 4 files changed, 28 insertions(+), 49 deletions(-) diff --git a/action.yml b/action.yml index 6ab08db..ed9d0ea 100644 --- a/action.yml +++ b/action.yml @@ -19,6 +19,10 @@ inputs: description: "Additional CLI flags to pass to flaglint" required: false default: "" + version: + description: "FlagLint version to use (e.g. latest, 0.8.0)" + required: false + default: "latest" node-version: description: "Node.js version to use" required: false @@ -38,9 +42,10 @@ runs: FLAGLINT_COMMAND: ${{ inputs.command }} FLAGLINT_DIRECTORY: ${{ inputs.directory }} FLAGLINT_EXTRA_ARGS: ${{ inputs.extra-args }} + FLAGLINT_VERSION: ${{ inputs.version }} run: | if [ "$FLAGLINT_COMMAND" = "validate" ]; then - npx flaglint@latest validate "$FLAGLINT_DIRECTORY" --no-direct-launchdarkly $FLAGLINT_EXTRA_ARGS + npx flaglint@"$FLAGLINT_VERSION" validate "$FLAGLINT_DIRECTORY" --no-direct-launchdarkly $FLAGLINT_EXTRA_ARGS else - npx flaglint@latest "$FLAGLINT_COMMAND" "$FLAGLINT_DIRECTORY" $FLAGLINT_EXTRA_ARGS + npx flaglint@"$FLAGLINT_VERSION" "$FLAGLINT_COMMAND" "$FLAGLINT_DIRECTORY" $FLAGLINT_EXTRA_ARGS fi diff --git a/src/config.ts b/src/config.ts index 3fc28de..3d282b3 100644 --- a/src/config.ts +++ b/src/config.ts @@ -15,11 +15,8 @@ export const FlagLintConfigSchema = z.object({ "**/*.d.ts", ]), // Governs which vendor SDK the scanner targets. - // Currently only "launchdarkly" is wired in the scanner. - // Other values are accepted for forward compatibility (v0.7+). - provider: z - .enum(["launchdarkly", "unleash", "growthbook", "custom"]) - .default("launchdarkly"), + // Only "launchdarkly" is wired; other values are rejected with a clear error. + provider: z.string().default("launchdarkly"), // TODO v0.3: replace minFileCount with real date-based staleness via git log minFileCount: z.number().int().min(0).default(0), wrappers: z @@ -55,6 +52,15 @@ export type ScanConfig = Pick { const candidates = configPath ? [configPath] : SEARCH_PATHS; @@ -73,8 +79,9 @@ export async function loadConfig(configPath?: string): Promise { throw new Error(`Error reading ${candidate}: ${String(err)}`); } + let parsed: FlagLintConfig; try { - return FlagLintConfigSchema.parse(raw); + parsed = FlagLintConfigSchema.parse(raw); } catch (err) { if (err instanceof ZodError) { const detail = err.errors.map((e) => `${e.path.join(".")}: ${e.message}`).join("; "); @@ -82,6 +89,8 @@ export async function loadConfig(configPath?: string): Promise { } throw err; } + assertSupportedProvider(parsed.provider, candidate); + return parsed; } return FlagLintConfigSchema.parse({}); diff --git a/src/config/tests/config.test.ts b/src/config/tests/config.test.ts index 36953f7..d4ea362 100644 --- a/src/config/tests/config.test.ts +++ b/src/config/tests/config.test.ts @@ -59,11 +59,9 @@ describe("loadConfig — partial config merges with defaults", () => { expect(config.minFileCount).toBe(0); }); - it("overrides provider while keeping other defaults", async () => { + it("throws when provider is set to an unsupported value (e.g. unleash)", async () => { const path = tracked(`flaglint-test-${Date.now()}.json`, { provider: "unleash" }); - const config = await loadConfig(path); - expect(config.provider).toBe("unleash"); - expect(config.outputDir).toBe("."); + await expect(loadConfig(path)).rejects.toThrow(/provider "unleash" is not supported/); }); it("overrides include array", async () => { @@ -87,7 +85,7 @@ describe("loadConfig — invalid config throws clear error", () => { it("throws when provider is an unknown value", async () => { const path = tracked(`flaglint-bad-${Date.now()}.json`, { provider: "unknown-provider" }); - await expect(loadConfig(path)).rejects.toThrow(/Error in/); + await expect(loadConfig(path)).rejects.toThrow(/provider "unknown-provider" is not supported/); }); it("throws when include is not an array", async () => { diff --git a/src/validator/index.ts b/src/validator/index.ts index 766d5fd..cbc6cc7 100644 --- a/src/validator/index.ts +++ b/src/validator/index.ts @@ -1,5 +1,6 @@ import { resolve } from "path"; import { pathToFileURL } from "url"; +import micromatch from "micromatch"; import type { ScanResult, FlagUsage, CallType } from "../types.js"; // ── Types ───────────────────────────────────────────────────────────────────── @@ -42,44 +43,10 @@ export interface ValidateOptions { // ── Bootstrap-file glob matching ────────────────────────────────────────────── -/** - * Returns true when `file` matches any of the `patterns`. - * - * Supports: - * exact path "src/provider/setup.ts" - * * wildcard "src/provider/*.ts" — matches within one directory level - * ** wildcard "src/bootstrap/**" — matches across directory levels - * - * No external dependency — avoids importing micromatch/minimatch directly. - */ export function matchesBootstrapPattern(file: string, patterns: string[]): boolean { - // Normalise leading "./" + if (patterns.length === 0) return false; const clean = (s: string): string => s.replace(/^\.\//, ""); - const cleanFile = clean(file); - - return patterns.some((pattern) => { - const cleanPattern = clean(pattern); - if (cleanFile === cleanPattern) return true; - - // Convert glob to a regular expression. - const regexStr = cleanPattern - // Escape regex-special chars except * and ? - .replace(/[.+^${}()|[\]\\]/g, "\\$&") - // Temporarily hide ** before processing single * - .replace(/\*\*/g, "\x00") - // Single * → match any sequence that does NOT cross a / - .replace(/\*/g, "[^/]*") - // ** → match anything including / - .replace(/\x00/g, ".*") - // ? → single non-separator char - .replace(/\?/g, "[^/]"); - - try { - return new RegExp(`^${regexStr}$`).test(cleanFile); - } catch { - return false; - } - }); + return micromatch.isMatch(clean(file), patterns.map(clean)); } // ── Core validation logic ─────────────────────────────────────────────────────