Skip to content

Commit 4ece737

Browse files
authored
Fix asset-only PR handling by fetching parent file content (#428)
When a PR only modifies asset files (e.g. assets/eip-1234/image.png), fetch the parent markdown from the default branch so the authors rule can extract authors and allow self-approval. - Fix split index bug (split("/")[2] → split("/")[1]) - Use raw mediaType to simplify content fetching - Handle 404 gracefully (fall through), re-throw unexpected errors - Add tests for EIP/ERC asset changes, parent-in-PR skip, and non-assets
1 parent a88342f commit 4ece737

File tree

3 files changed

+167
-2
lines changed

3 files changed

+167
-2
lines changed

jest.config.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,7 @@ export default {
44
transform: {
55
"^.+\\.(ts|tsx)$": "ts-jest",
66
},
7+
moduleNameMapper: {
8+
"^(\\.{1,2}/.*)\\.js$": "$1",
9+
},
710
};

src/rules/__tests__/assets.test.ts

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
import { Octokit } from "../../types";
2+
import checkAssets from "../assets";
3+
4+
jest.mock("@actions/github", () => ({
5+
__esModule: true,
6+
default: {
7+
context: {
8+
repo: {
9+
owner: "ethereum",
10+
repo: "EIPs",
11+
},
12+
},
13+
},
14+
}));
15+
16+
jest.mock("../../process", () => ({
17+
__esModule: true,
18+
default: jest.fn(
19+
(
20+
_o: unknown,
21+
_c: unknown,
22+
files: { filename: string; contents?: string }[],
23+
) =>
24+
files.flatMap((f) => {
25+
const authors = f.contents
26+
?.match(/author:\s*(.+)/)?.[1]
27+
?.match(/@(\w+)/g)
28+
?.map((s) => s.slice(1));
29+
if (!authors?.length) return [];
30+
return [
31+
{
32+
name: "authors",
33+
reviewers: authors,
34+
min: 1,
35+
pr_approval: true,
36+
annotation: { file: f.filename },
37+
},
38+
];
39+
}),
40+
),
41+
}));
42+
43+
const frontmatter =
44+
"---\nstatus: Draft\ncategory: ERC\nauthor: Alice (@alice), Bob (@bob)\n---\nHello!";
45+
function makeFakeOctokit() {
46+
return {
47+
rest: {
48+
repos: {
49+
getContent: jest.fn().mockResolvedValue({
50+
data: frontmatter,
51+
}),
52+
},
53+
},
54+
} as unknown as Octokit;
55+
}
56+
57+
describe("checkAssets", () => {
58+
test("Should require author approval for asset-only EIP changes", async () => {
59+
const fakeOctokit = makeFakeOctokit();
60+
const result = await checkAssets(
61+
fakeOctokit,
62+
{ erc: ["editor1", "editor2", "editor3"] },
63+
[
64+
{
65+
filename: "assets/eip-1234/image.png",
66+
status: "modified",
67+
},
68+
],
69+
);
70+
expect(result).toMatchObject([
71+
{
72+
name: "authors",
73+
reviewers: ["alice", "bob"],
74+
min: 1,
75+
pr_approval: true,
76+
annotation: {
77+
file: "EIPS/eip-1234.md",
78+
},
79+
},
80+
]);
81+
});
82+
83+
test("Should require author approval for asset-only ERC changes", async () => {
84+
const fakeOctokit = makeFakeOctokit();
85+
const result = await checkAssets(
86+
fakeOctokit,
87+
{ erc: ["editor1", "editor2", "editor3"] },
88+
[
89+
{
90+
filename: "assets/erc-5678/diagram.svg",
91+
status: "modified",
92+
},
93+
],
94+
);
95+
expect(result).toMatchObject([
96+
{
97+
name: "authors",
98+
reviewers: ["alice", "bob"],
99+
min: 1,
100+
pr_approval: true,
101+
annotation: {
102+
file: "ERCS/erc-5678.md",
103+
},
104+
},
105+
]);
106+
});
107+
108+
test("Should skip when parent EIP file is also in the PR", async () => {
109+
const fakeOctokit = makeFakeOctokit();
110+
const result = await checkAssets(
111+
fakeOctokit,
112+
{ erc: ["editor1", "editor2", "editor3"] },
113+
[
114+
{
115+
filename: "assets/eip-1234/image.png",
116+
status: "modified",
117+
},
118+
{
119+
filename: "EIPS/eip-1234.md",
120+
status: "modified",
121+
contents: frontmatter,
122+
previous_contents: frontmatter,
123+
},
124+
],
125+
);
126+
expect(result).toEqual([]);
127+
});
128+
129+
test("Should return empty for non-asset files", async () => {
130+
const fakeOctokit = makeFakeOctokit();
131+
const result = await checkAssets(
132+
fakeOctokit,
133+
{ erc: ["editor1", "editor2", "editor3"] },
134+
[
135+
{
136+
filename: "EIPS/eip-1234.md",
137+
status: "modified",
138+
contents: frontmatter,
139+
previous_contents: frontmatter,
140+
},
141+
],
142+
);
143+
expect(result).toEqual([]);
144+
});
145+
});

src/rules/assets.ts

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import processFiles from "../process.js";
22
import { Config, File, Octokit, Rule } from "../types.js";
3+
import github from "@actions/github";
34

45
export default async function (
56
octokit: Octokit,
@@ -14,15 +15,15 @@ export default async function (
1415
file.filename.startsWith("assets/eip-") &&
1516
!files.some(
1617
(f) =>
17-
f.filename == `EIPS/${file.filename.split("/")[2]}.md`,
18+
f.filename == `EIPS/${file.filename.split("/")[1]}.md`,
1819
)
1920
) {
2021
filename = `EIPS/${file.filename.split("/")[1]}.md`;
2122
} else if (
2223
file.filename.startsWith("assets/erc-") &&
2324
!files.some(
2425
(f) =>
25-
f.filename == `ERCS/${file.filename.split("/")[2]}.md`,
26+
f.filename == `ERCS/${file.filename.split("/")[1]}.md`,
2627
)
2728
) {
2829
filename = `ERCS/${file.filename.split("/")[1]}.md`;
@@ -33,10 +34,26 @@ export default async function (
3334
if (files.some((file) => file.filename == filename)) {
3435
return []; // Already covered by the relevant rules, so avoid potential conflicts by short circuiting
3536
}
37+
let contents: string | undefined;
38+
try {
39+
contents = (
40+
await octokit.rest.repos.getContent({
41+
...github.context.repo,
42+
path: filename,
43+
mediaType: { format: "raw" },
44+
})
45+
).data as unknown as string;
46+
} catch (e) {
47+
if (!(e instanceof Object && "status" in e && e.status === 404))
48+
throw e;
49+
}
50+
3651
return processFiles(octokit, config, [
3752
{
3853
filename,
3954
status: "modified",
55+
contents,
56+
previous_contents: contents,
4057
},
4158
]);
4259
}),

0 commit comments

Comments
 (0)