Skip to content

Commit 59db9a8

Browse files
committed
feat: Allow ignoring files, support config file
1 parent 498a730 commit 59db9a8

File tree

8 files changed

+348
-4
lines changed

8 files changed

+348
-4
lines changed

action.yml

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,17 @@ branding:
88

99
inputs:
1010
commit-branch:
11-
description: The branch to commit changes to
11+
description: The branch to commit changes to. Can be overridden by config file.
1212
required: false
1313
type: string
1414
default: gha/actions-sync
15+
config-file:
16+
description: |
17+
Path to the configuration file relative to the repository root.
18+
Defaults to .github/actions-sync.yml
19+
required: false
20+
type: string
21+
default: .github/actions-sync.yml
1522
commit-message:
1623
description: The commit message to use when committing changes
1724
required: false
@@ -75,6 +82,12 @@ inputs:
7582
required: false
7683
type: string
7784
default: main
85+
ignore-files:
86+
description: |
87+
Newline-separated list of file paths to ignore during sync.
88+
Paths are relative to the templates directory. Can be overridden by config file.
89+
required: false
90+
type: string
7891

7992
outputs:
8093
pull_request_url:

package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,13 @@
2727
"@octokit/plugin-retry": "^3.0.9",
2828
"@octokit/plugin-throttling": "^3.7.0",
2929
"handlebars": "^4.7.7",
30+
"js-yaml": "^4.1.1",
3031
"lodash": "^4.17.21",
32+
"minimatch": "^10.1.1",
3133
"uuid": "^9.0.0"
3234
},
3335
"devDependencies": {
36+
"@types/js-yaml": "^4.0.9",
3437
"@types/lodash": "^4.14.199",
3538
"@types/node": "^20.4.1",
3639
"@types/uuid": "^9.0.2",

src/config.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,9 @@ export type Config = {
99
commitMessage: string;
1010
commitUserEmail: string;
1111
commitUserName: string;
12+
configFilePath: string;
1213
fullPath: string;
14+
ignoreFiles: string[];
1315
path: string;
1416
prAssignee?: string;
1517
prBody: string;
@@ -30,12 +32,16 @@ export function getConfig(): Config {
3032
const workspace = process.env.GITHUB_WORKSPACE;
3133
ok(workspace, "Expected GITHUB_WORKSPACE to be defined");
3234

35+
const configFile = core.getInput("config-file", { required: false }) || ".github/actions-sync.yml";
36+
3337
return {
3438
commitBranch: core.getInput("commit-branch", { required: true }),
3539
commitMessage: core.getInput("commit-message", { required: true }),
3640
commitUserEmail: core.getInput("commit-user-email", { required: true }),
3741
commitUserName: core.getInput("commit-user-name", { required: true }),
42+
configFilePath: join(workspace, path, configFile),
3843
fullPath: join(workspace, path),
44+
ignoreFiles: core.getMultilineInput("ignore-files", { required: false }),
3945
path: path,
4046
prBody: core.getInput("pr-body", { required: false }),
4147
prEnabled: core.getBooleanInput("pr-enabled", { required: true }),

src/configFile.test.ts

Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
import { mkdtemp, rm, writeFile, mkdir } from "fs/promises";
2+
import { afterEach, beforeEach, describe, expect, it } from "vitest";
3+
import { join } from "path";
4+
import { tmpdir } from "os";
5+
6+
import { Config } from "./config";
7+
import { loadConfigFile } from "./configFile";
8+
9+
interface LocalTestContext {
10+
config: Config;
11+
tempDir: string;
12+
}
13+
14+
describe.concurrent("configFile", () => {
15+
beforeEach<LocalTestContext>(async (ctx) => {
16+
const tempDir = await mkdtemp(join(tmpdir(), "actions-sync-config"));
17+
await mkdir(join(tempDir, ".github"), { recursive: true });
18+
19+
ctx.tempDir = tempDir;
20+
ctx.config = {
21+
commitBranch: "gha/actions-sync",
22+
commitMessage: "chore: sync files",
23+
commitUserEmail: "test@example.com",
24+
commitUserName: "testing",
25+
configFilePath: join(tempDir, ".github/actions-sync.yml"),
26+
fullPath: tempDir,
27+
ignoreFiles: [],
28+
path: "",
29+
prAssignee: "",
30+
prBody: "",
31+
prEnabled: true,
32+
prLabels: [],
33+
prReviewUsers: [],
34+
prTitle: "chore: sync files",
35+
syncAuth: "",
36+
syncPath: "",
37+
syncRepository: "test/test",
38+
syncTree: "main",
39+
templateVariables: {},
40+
};
41+
});
42+
43+
afterEach<LocalTestContext>(async (ctx) => {
44+
await rm(ctx.tempDir, { recursive: true });
45+
});
46+
47+
it<LocalTestContext>("returns original config when no config file exists", async (ctx) => {
48+
const result = await loadConfigFile(ctx.config);
49+
expect(result).toEqual(ctx.config);
50+
});
51+
52+
it<LocalTestContext>("loads ignore-files from config file", async (ctx) => {
53+
await writeFile(
54+
ctx.config.configFilePath,
55+
`ignore-files:
56+
- .credo.exs
57+
- .formatter.exs
58+
`,
59+
);
60+
61+
const result = await loadConfigFile(ctx.config);
62+
expect(result.ignoreFiles).toEqual([".credo.exs", ".formatter.exs"]);
63+
});
64+
65+
it<LocalTestContext>("action input takes precedence over config file for ignore-files", async (ctx) => {
66+
await writeFile(
67+
ctx.config.configFilePath,
68+
`ignore-files:
69+
- .credo.exs
70+
`,
71+
);
72+
73+
const configWithInput = {
74+
...ctx.config,
75+
ignoreFiles: [".formatter.exs"],
76+
};
77+
78+
const result = await loadConfigFile(configWithInput);
79+
expect(result.ignoreFiles).toEqual([".formatter.exs"]);
80+
});
81+
82+
it<LocalTestContext>("loads commit-branch from config file", async (ctx) => {
83+
await writeFile(
84+
ctx.config.configFilePath,
85+
`commit-branch: custom/branch
86+
`,
87+
);
88+
89+
// Simulate empty action input by setting to empty string
90+
const configWithEmptyInput = {
91+
...ctx.config,
92+
commitBranch: "",
93+
};
94+
95+
const result = await loadConfigFile(configWithEmptyInput);
96+
expect(result.commitBranch).toBe("custom/branch");
97+
});
98+
99+
it<LocalTestContext>("loads pr-enabled from config file", async (ctx) => {
100+
await writeFile(
101+
ctx.config.configFilePath,
102+
`pr-enabled: false
103+
`,
104+
);
105+
106+
const result = await loadConfigFile(ctx.config);
107+
expect(result.prEnabled).toBe(false);
108+
});
109+
110+
it<LocalTestContext>("loads pr-labels from config file", async (ctx) => {
111+
await writeFile(
112+
ctx.config.configFilePath,
113+
`pr-labels:
114+
- sync
115+
- automated
116+
`,
117+
);
118+
119+
const result = await loadConfigFile(ctx.config);
120+
expect(result.prLabels).toEqual(["sync", "automated"]);
121+
});
122+
123+
it<LocalTestContext>("action input takes precedence over config file for pr-labels", async (ctx) => {
124+
await writeFile(
125+
ctx.config.configFilePath,
126+
`pr-labels:
127+
- from-file
128+
`,
129+
);
130+
131+
const configWithInput = {
132+
...ctx.config,
133+
prLabels: ["from-action"],
134+
};
135+
136+
const result = await loadConfigFile(configWithInput);
137+
expect(result.prLabels).toEqual(["from-action"]);
138+
});
139+
140+
it<LocalTestContext>("handles multiple config options together", async (ctx) => {
141+
await writeFile(
142+
ctx.config.configFilePath,
143+
`commit-branch: feature/sync
144+
pr-title: "feat: sync configuration"
145+
pr-labels:
146+
- sync
147+
ignore-files:
148+
- "*.md"
149+
`,
150+
);
151+
152+
const configWithEmptyInputs = {
153+
...ctx.config,
154+
commitBranch: "",
155+
prTitle: "",
156+
};
157+
158+
const result = await loadConfigFile(configWithEmptyInputs);
159+
expect(result.commitBranch).toBe("feature/sync");
160+
expect(result.prTitle).toBe("feat: sync configuration");
161+
expect(result.prLabels).toEqual(["sync"]);
162+
expect(result.ignoreFiles).toEqual(["*.md"]);
163+
});
164+
165+
it<LocalTestContext>("handles invalid YAML gracefully", async (ctx) => {
166+
await writeFile(ctx.config.configFilePath, "invalid: yaml: content: [");
167+
168+
const result = await loadConfigFile(ctx.config);
169+
// Should return original config when YAML is invalid
170+
expect(result).toEqual(ctx.config);
171+
});
172+
});

src/configFile.ts

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import * as core from "@actions/core";
2+
import { readFile } from "fs/promises";
3+
import { load as loadYaml } from "js-yaml";
4+
5+
import { Config } from "./config";
6+
7+
/**
8+
* Schema for the actions-sync.yml configuration file.
9+
* All fields are optional and will override action inputs when specified.
10+
*/
11+
export type ConfigFile = {
12+
"commit-branch"?: string;
13+
"commit-message"?: string;
14+
"commit-user-email"?: string;
15+
"commit-user-name"?: string;
16+
"ignore-files"?: string[];
17+
"pr-body"?: string;
18+
"pr-enabled"?: boolean;
19+
"pr-labels"?: string[];
20+
"pr-review-users"?: string[];
21+
"pr-title"?: string;
22+
};
23+
24+
/**
25+
* Loads the actions-sync.yml configuration file from the target repository
26+
* and merges it with the existing config. Action workflow inputs take precedence
27+
* over values in the config file.
28+
*/
29+
export async function loadConfigFile(config: Config): Promise<Config> {
30+
let fileConfig: ConfigFile = {};
31+
32+
try {
33+
const configFileContent = await readFile(config.configFilePath, "utf8");
34+
const parsed = loadYaml(configFileContent);
35+
36+
if (parsed && typeof parsed === "object") {
37+
fileConfig = parsed as ConfigFile;
38+
core.info(`Loaded configuration from ${config.configFilePath}`);
39+
}
40+
} catch (err) {
41+
if ((err as NodeJS.ErrnoException).code === "ENOENT") {
42+
core.debug(`No config file found at ${config.configFilePath}`);
43+
} else {
44+
core.warning(
45+
`Failed to load config file at ${config.configFilePath}: ${err}`,
46+
);
47+
}
48+
return config;
49+
}
50+
51+
// Merge config file values with action inputs.
52+
// Config file values are used as defaults, action inputs take precedence.
53+
return {
54+
...config,
55+
commitBranch: config.commitBranch || fileConfig["commit-branch"] || config.commitBranch,
56+
commitMessage: config.commitMessage || fileConfig["commit-message"] || config.commitMessage,
57+
commitUserEmail: config.commitUserEmail || fileConfig["commit-user-email"] || config.commitUserEmail,
58+
commitUserName: config.commitUserName || fileConfig["commit-user-name"] || config.commitUserName,
59+
ignoreFiles: config.ignoreFiles.length > 0
60+
? config.ignoreFiles
61+
: fileConfig["ignore-files"] || [],
62+
prBody: config.prBody || fileConfig["pr-body"] || config.prBody,
63+
prEnabled: fileConfig["pr-enabled"] !== undefined && config.prEnabled === true
64+
? fileConfig["pr-enabled"]
65+
: config.prEnabled,
66+
prLabels: config.prLabels.length > 0
67+
? config.prLabels
68+
: fileConfig["pr-labels"] || [],
69+
prReviewUsers: config.prReviewUsers.length > 0
70+
? config.prReviewUsers
71+
: fileConfig["pr-review-users"] || [],
72+
prTitle: config.prTitle || fileConfig["pr-title"] || config.prTitle,
73+
};
74+
}

src/index.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,13 @@ import {
77
createPr,
88
} from "./git";
99
import { getConfig } from "./config";
10+
import { loadConfigFile } from "./configFile";
1011
import { templateFiles } from "./templates";
1112
import { runScripts } from "./scripts";
1213

1314
export async function run() {
14-
const config = getConfig();
15+
let config = getConfig();
16+
config = await loadConfigFile(config);
1517

1618
await configureRepository(config);
1719
await cloneRepository(config);

0 commit comments

Comments
 (0)