From af6fdb96b0c559f8c0728e82eeeb5b66786183e2 Mon Sep 17 00:00:00 2001 From: Vivswan Shah <58091053+Vivswan@users.noreply.github.com> Date: Sun, 11 Oct 2026 23:37:57 -0400 Subject: [PATCH] fix(inputs): refuse a bad repos list and misplaced filters when the inputs are parsed A repos list mixing "*" with names, a bad entry in it, and discovery filters set where no "*" discovery runs were refused inside resolveTargets, after the repos directory was read, so an unreadable directory reported itself and hid the input error. parseConfig now refuses them in the multi and snapshot-dir arms, before any file is read; resolveTargets keeps the same refusals for a library caller that builds the config by hand. --- src/flows/multi.ts | 2 ++ src/flows/run-config.ts | 26 +++++++++++++++++++++++++- test/flows/inputs.test.ts | 38 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 65 insertions(+), 1 deletion(-) diff --git a/src/flows/multi.ts b/src/flows/multi.ts index 660497a6..a761f82b 100644 --- a/src/flows/multi.ts +++ b/src/flows/multi.ts @@ -320,6 +320,8 @@ function maskFleet( const knownVisibility = new Map(); // Private slugs discovery filtered out are masked but never placeholdered. const filteredPrivateSlugs: Private[] = []; + // parseConfig refuses a bad repos list and misplaced filters before any file is read; a library caller that + // builds the config by hand meets the same refusals here. if (cfg.reposInput) { const parsed = yield* parseReposInput(cfg.reposInput); if (parsed.discover) { diff --git a/src/flows/run-config.ts b/src/flows/run-config.ts index 1d748817..746da9b9 100644 --- a/src/flows/run-config.ts +++ b/src/flows/run-config.ts @@ -5,7 +5,7 @@ */ import { err, ok, type Result, safeTry } from "neverthrow"; -import { LIST_SEPARATOR } from "../discovery/repos-input.js"; +import { LIST_SEPARATOR, parseReposInput } from "../discovery/repos-input.js"; import { LAYERINGS } from "../engine/layers.js"; import type { Problem } from "../problem.js"; import { PRIVATE_REPORT_CHANNELS } from "../report/delivery.js"; @@ -96,6 +96,28 @@ function readSnapshotBase(input: Inputs, env: ConfigEnv) { }); } +/** + * The target problems the inputs alone decide: a repos list that mixes "*" with names or carries a bad entry, and + * discovery filters set where no "*" discovery runs. Refused here, before any file is read, so an unreadable + * repos-dir cannot hide them; resolveTargets meets the same problems for a library caller that builds the config + * by hand. + */ +function refuseTargetInputs( + reposInput: string, + discoveryFiltersSet: readonly string[], +): Result { + const filtersWithout = (targets: "explicit-repos" | "repos-dir"): Result => + discoveryFiltersSet.length > 0 + ? err({ code: "discovery-filters-without-wildcard", filters: discoveryFiltersSet, targets }) + : ok(undefined); + if (!reposInput) { + return filtersWithout("repos-dir"); + } + return parseReposInput(reposInput).andThen((parsed) => + parsed.discover ? ok(undefined) : filtersWithout("explicit-repos"), + ); +} + /** The file arm: one repository, the fleet inputs refused. */ function parseSnapshotFileArm( input: Inputs, @@ -175,6 +197,7 @@ function parseSnapshotConfig( if (!reposInput && !reposDir) { return err({ code: "input-snapshot-dir-without-targets" }); } + yield* refuseTargetInputs(reposInput, filters.discoveryFiltersSet); return ok({ ...base, form: "dir" as const, @@ -276,6 +299,7 @@ export function parseConfig( if (settingsFile !== DEFAULT_SETTINGS_FILE) { return err({ code: "input-settings-file-with-multi" }); } + yield* refuseTargetInputs(reposInput, discoveryFiltersSet); const adminOwner = githubRepository.split("/")[0] ?? ""; return ok({ ...common, diff --git a/test/flows/inputs.test.ts b/test/flows/inputs.test.ts index 24ba25b4..a85d1941 100644 --- a/test/flows/inputs.test.ts +++ b/test/flows/inputs.test.ts @@ -553,3 +553,41 @@ describe("mode: snapshot", () => { ); }); }); + +describe("target inputs the file system cannot change", () => { + // Before, these were refused inside resolveTargets after the repos directory was read, so an unreadable directory + // reported itself and hid the input error; parseConfig never touches the directory. + const FLEET = { GITHUB_REPOSITORY: "admin/fleet" }; + test.each<[string, Partial>, Problem]>([ + [ + "a repos list mixing * with names", + { repos: "*,o/r" }, + { code: "repos-input-wildcard-mixed" }, + ], + [ + "a repos list with a bad entry", + { repos: "o/r,not-a-slug" }, + { code: "repos-input-invalid-entries", invalid: ["not-a-slug"], duplicated: [] }, + ], + [ + "discovery filters beside explicit repos", + { repos: "o/r", forks: "exclude" }, + { code: "discovery-filters-without-wildcard", filters: ["forks"], targets: "explicit-repos" }, + ], + [ + "discovery filters beside repos-dir alone", + { "repos-dir": "repos", forks: "exclude" }, + { code: "discovery-filters-without-wildcard", filters: ["forks"], targets: "repos-dir" }, + ], + ])( + "%s is refused when the inputs are parsed, for apply and for a snapshot dir", + (_case, inputs, problem) => { + expect(rejection(parse({ token: "t", ...inputs }, FLEET))).toEqual(problem); + expect( + rejection( + parse({ token: "t", mode: "snapshot", "snapshot-dir": "snaps", ...inputs }, FLEET), + ), + ).toEqual(problem); + }, + ); +});