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); + }, + ); +});