diff --git a/app/router.js b/app/router.js index 524ce51..4d93faa 100644 --- a/app/router.js +++ b/app/router.js @@ -45,12 +45,27 @@ export async function router(auth, id, payload, verbose) { } try { - const { config } = await octokit.config.get({ - owner: payload.repository.owner.login, - repo: payload.repository.name, - path: ".github/comment-ops.yml", - defaults: (configs) => deepmerge.all([defaultConfig, ...configs]), + // noinspection JSUnusedGlobalSymbols + const owner = payload.repository.owner.login; + const repoName = payload.repository.name; + const configPath = ".github/comment-ops.yml"; + + // Fetch org-level config from the .github repo + const { files: orgFiles } = await octokit.config.get({ + owner, + repo: ".github", + path: configPath, + }); + + // Fetch repo-level config (files[0] is always the requested repo's file) + const { files: repoFiles } = await octokit.config.get({ + owner, + repo: repoName, + path: configPath, }); + + // Merge in order: default → org → repo + const config = mergeConfigs(defaultConfig, orgFiles, repoFiles); validateConfig(config); for (const command of commands) { @@ -75,3 +90,23 @@ function getCommentNodeId(payload) { return payload.comment.node_id; } + +export function mergeConfigs(defaults, orgFiles, repoFiles) { + // Only use repo files if the repo has its own config, to avoid including + // the org .github fallthrough that the plugin adds when no repo config exists + const hasRepoConfig = !!repoFiles[0]?.config; + + const orgConfigs = orgFiles + .map((f) => f.config) + .filter(Boolean) + .reverse(); + const repoConfigs = hasRepoConfig + ? repoFiles + .map((f) => f.config) + .filter(Boolean) + .reverse() + : []; + + // Merge in order: default → org → repo + return deepmerge.all([defaults, ...orgConfigs, ...repoConfigs]); +} diff --git a/app/router.test.js b/app/router.test.js new file mode 100644 index 0000000..206671f --- /dev/null +++ b/app/router.test.js @@ -0,0 +1,116 @@ +import { mergeConfigs } from "./router.js"; +import { defaultConfig } from "./default-config.js"; + +describe("mergeConfigs", () => { + test("returns default config when no org or repo config", () => { + const orgFiles = [{ config: undefined }]; + const repoFiles = [{ config: undefined }]; + + const result = mergeConfigs(defaultConfig, orgFiles, repoFiles); + + expect(result).toEqual(defaultConfig); + }); + + test("merges default and org config when only org config exists", () => { + const orgFiles = [{ config: { commands: { close: { enabled: true } } } }]; + const repoFiles = [{ config: undefined }]; + + const result = mergeConfigs(defaultConfig, orgFiles, repoFiles); + + expect(result.commands.close.enabled).toEqual(true); + expect(result.commands.label.enabled).toEqual(false); + }); + + test("merges default and repo config when only repo config exists", () => { + const orgFiles = [{ config: undefined }]; + const repoFiles = [{ config: { commands: { reopen: { enabled: true } } } }]; + + const result = mergeConfigs(defaultConfig, orgFiles, repoFiles); + + expect(result.commands.reopen.enabled).toEqual(true); + expect(result.commands.close.enabled).toEqual(false); + }); + + test("merges default, org, and repo configs when all exist", () => { + const orgFiles = [ + { + config: { + commands: { + close: { enabled: true }, + label: { enabled: true, allowedLabels: [] }, + }, + }, + }, + ]; + const repoFiles = [{ config: { commands: { reopen: { enabled: true } } } }]; + + const result = mergeConfigs(defaultConfig, orgFiles, repoFiles); + + expect(result.commands.close.enabled).toEqual(true); + expect(result.commands.label.enabled).toEqual(true); + expect(result.commands.reopen.enabled).toEqual(true); + }); + + test("repo config overrides org config", () => { + const orgFiles = [{ config: { commands: { close: { enabled: true } } } }]; + const repoFiles = [{ config: { commands: { close: { enabled: false } } } }]; + + const result = mergeConfigs(defaultConfig, orgFiles, repoFiles); + + expect(result.commands.close.enabled).toEqual(false); + }); + + test("org config overrides default config", () => { + const orgFiles = [ + { + config: { + commands: { label: { enabled: true, allowedLabels: ["bug"] } }, + }, + }, + ]; + const repoFiles = [{ config: undefined }]; + + const result = mergeConfigs(defaultConfig, orgFiles, repoFiles); + + expect(result.commands.label.enabled).toEqual(true); + expect(result.commands.label.allowedLabels).toEqual(["bug"]); + }); + + test("handles _extends chain in org files", () => { + // When org config has _extends, plugin returns multiple files (base first, extending last) + // compose-config-get reverses them so mergeConfigs receives them in this order + const orgFiles = [ + { config: { commands: { close: { enabled: true } } } }, // org config + { + config: { + commands: { + close: { enabled: false }, + label: { enabled: true, allowedLabels: [] }, + }, + }, + }, // base extended config + ]; + const repoFiles = [{ config: undefined }]; + + const result = mergeConfigs(defaultConfig, orgFiles, repoFiles); + + // Extended config is first after reverse, org config overrides it + expect(result.commands.close.enabled).toEqual(true); + expect(result.commands.label.enabled).toEqual(true); + }); + + test("handles _extends chain in repo files", () => { + const orgFiles = [{ config: undefined }]; + // repo config extends another config; plugin returns them with repo first, extended second + const repoFiles = [ + { config: { commands: { reopen: { enabled: true } } } }, // repo config + { config: { commands: { close: { enabled: true } } } }, // base extended config + ]; + + const result = mergeConfigs(defaultConfig, orgFiles, repoFiles); + + // extended config comes first after reverse, repo config overrides it + expect(result.commands.reopen.enabled).toEqual(true); + expect(result.commands.close.enabled).toEqual(true); + }); +});