Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 40 additions & 5 deletions app/router.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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]);
}
116 changes: 116 additions & 0 deletions app/router.test.js
Original file line number Diff line number Diff line change
@@ -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);
});
});
Loading