Skip to content

Commit 4603593

Browse files
committed
add check and update artifact name for build summary
1 parent 99f02ef commit 4603593

5 files changed

Lines changed: 246 additions & 51 deletions

File tree

package-lock.json

Lines changed: 9 additions & 9 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

plugins/+buildframework/BuildSummaryPlugin.m

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,15 @@
1010
function runTaskGraph(plugin, pluginData)
1111
runTaskGraph@matlab.buildtool.plugins.BuildRunnerPlugin(plugin, pluginData);
1212

13-
[fID, msg] = fopen(fullfile(getenv("RUNNER_TEMP") ,"buildSummary" + getenv("GITHUB_RUN_ID") + ".json"), "w");
14-
if fID == -1
15-
warning("buildframework:BuildSummaryPlugin:UnableToOpenFile","Unable to open a file required to create the MATLAB build summary table: %s", msg);
16-
else
17-
closeFile = onCleanup(@()fclose(fID));
18-
s = jsonencode(plugin.TaskDetails);
19-
fprintf(fID, "%s",s);
13+
if strcmpi(getenv("MW_GENERATE_JOB_SUMMARY"), "true")
14+
[fID, msg] = fopen(fullfile(getenv("RUNNER_TEMP"), "buildSummary" + getenv("GITHUB_ACTION") + "_" + string(datetime('now', 'Format', 'yyyyMMdd_HHmmss_SSS')) + ".json"), "w");
15+
if fID == -1
16+
warning("buildframework:BuildSummaryPlugin:UnableToOpenFile","Unable to open a file required to create the MATLAB build summary table: %s", msg);
17+
else
18+
closeFile = onCleanup(@()fclose(fID));
19+
s = jsonencode(plugin.TaskDetails);
20+
fprintf(fID, "%s",s);
21+
end
2022
end
2123
end
2224

plugins/+buildframework/ParallelizableBuildSummaryPlugin.m

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -34,14 +34,16 @@ function runBuild(plugin, pluginData)
3434
end
3535

3636
% Write to file
37-
folder = fileparts(plugin.TempFolder);
38-
[fID, msg] = fopen(fullfile(folder, "buildSummary" + getenv("GITHUB_RUN_ID") + ".json"), "w");
39-
if fID == -1
40-
warning("buildframework:BuildSummaryPlugin:UnableToOpenFile","Unable to open a file required to create the MATLAB build summary table: %s", msg);
41-
else
42-
closeFile = onCleanup(@()fclose(fID));
43-
s = jsonencode(taskDetails);
44-
fprintf(fID, "%s", s);
37+
if strcmpi(getenv("MW_GENERATE_JOB_SUMMARY"), "true")
38+
folder = fileparts(plugin.TempFolder);
39+
[fID, msg] = fopen(fullfile(folder, "buildSummary" + getenv("GITHUB_ACTION") + "_" + string(datetime('now', 'Format', 'yyyyMMdd_HHmmss_SSS')) + ".json"), "w");
40+
if fID == -1
41+
warning("buildframework:BuildSummaryPlugin:UnableToOpenFile","Unable to open a file required to create the MATLAB build summary table: %s", msg);
42+
else
43+
closeFile = onCleanup(@()fclose(fID));
44+
s = jsonencode(taskDetails);
45+
fprintf(fID, "%s", s);
46+
end
4547
end
4648
end
4749

src/buildSummary.ts

Lines changed: 27 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
// Copyright 2024-25 The MathWorks, Inc.
1+
// Copyright 2024-26 The MathWorks, Inc.
22
import * as core from "@actions/core";
33
import { join } from "path";
4-
import { readFileSync, unlinkSync, existsSync } from "fs";
4+
import { readFileSync, unlinkSync, readdirSync } from "fs";
55

66
export function addSummary(taskSummaryTableRows: string[][]) {
77
try {
@@ -43,24 +43,43 @@ export function interpretSkipReason(skipReason: string) {
4343
}
4444
}
4545

46-
export function processAndAddBuildSummary(runnerTemp: string, runId: string) {
46+
export function processAndAddBuildSummary(runnerTemp: string, actionName: string) {
47+
const filePrefix = `buildSummary${actionName}_`;
48+
const fileSuffix = `.json`;
49+
50+
let buildSummaryFiles: string[] = [];
51+
try {
52+
buildSummaryFiles = readdirSync(runnerTemp)
53+
.filter((file) => file.startsWith(filePrefix) && file.endsWith(fileSuffix))
54+
.sort();
55+
} catch (e) {
56+
console.error(
57+
`An error occurred while finding build summary file(s) in directory ${runnerTemp}:`,
58+
e,
59+
);
60+
return;
61+
}
62+
63+
if (buildSummaryFiles.length === 0) {
64+
return;
65+
}
66+
4767
const header = [
4868
{ data: "MATLAB Task", header: true },
4969
{ data: "Status", header: true },
5070
{ data: "Description", header: true },
5171
{ data: "Duration (HH:mm:ss)", header: true },
5272
];
5373

54-
const filePath: string = join(runnerTemp, `buildSummary${runId}.json`);
55-
let taskSummaryTable;
56-
if (existsSync(filePath)) {
74+
for (const fileName of buildSummaryFiles) {
75+
const filePath = join(runnerTemp, fileName);
5776
try {
5877
const buildSummary = readFileSync(filePath, { encoding: "utf8" });
5978
const rows = getSummaryRows(buildSummary);
60-
taskSummaryTable = [header, ...rows];
79+
const taskSummaryTable = [header, ...rows];
80+
addSummary(taskSummaryTable);
6181
} catch (e) {
6282
console.error("An error occurred while reading the build summary file:", e);
63-
return;
6483
} finally {
6584
try {
6685
unlinkSync(filePath);
@@ -71,6 +90,5 @@ export function processAndAddBuildSummary(runnerTemp: string, runId: string) {
7190
);
7291
}
7392
}
74-
addSummary(taskSummaryTable);
7593
}
7694
}

src/buildSummary.unit.test.ts

Lines changed: 191 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
// Copyright 2024-26 The MathWorks, Inc.
22

33
import { jest, describe, it, expect, beforeEach } from "@jest/globals";
4+
import * as path from "path";
5+
import * as nodeFs from "fs";
46

57
jest.unstable_mockModule("@actions/core", () => ({
68
summary: {
@@ -10,17 +12,56 @@ jest.unstable_mockModule("@actions/core", () => ({
1012
},
1113
}));
1214

15+
// Mock fs, passing through real implementations except unlinkSync
16+
const mockUnlinkSync = jest.fn();
17+
jest.unstable_mockModule("fs", () => ({
18+
readFileSync: nodeFs.readFileSync,
19+
existsSync: nodeFs.existsSync,
20+
writeFileSync: nodeFs.writeFileSync,
21+
readdirSync: nodeFs.readdirSync,
22+
unlinkSync: mockUnlinkSync,
23+
}));
24+
1325
const core = await import("@actions/core");
26+
const fs = await import("fs");
1427
const buildSummary = await import("./buildSummary.js");
1528

29+
const runnerTemp = path.join(import.meta.dirname, "..");
30+
31+
function safeDelete(filePath: string) {
32+
try {
33+
nodeFs.unlinkSync(filePath);
34+
} catch (e) {
35+
/* ignore */
36+
}
37+
}
38+
39+
const validBuildData = JSON.stringify([
40+
{
41+
name: "compile",
42+
failed: false,
43+
skipped: false,
44+
description: "Compile source",
45+
duration: "00:00:10",
46+
},
47+
{
48+
name: "test",
49+
failed: true,
50+
skipped: false,
51+
description: "Run tests",
52+
duration: "00:00:25",
53+
},
54+
]);
55+
1656
beforeEach(() => {
17-
(core.summary.addTable as jest.Mock).mockReturnThis();
18-
(core.summary.addHeading as jest.Mock).mockReturnThis();
19-
(core.summary.write as jest.Mock).mockReturnThis();
57+
(core.summary.addTable as jest.Mock).mockClear();
58+
(core.summary.addHeading as jest.Mock).mockClear();
59+
(core.summary.write as jest.Mock).mockClear();
60+
mockUnlinkSync.mockReset();
2061
});
2162

22-
describe("summaryGeneration", () => {
23-
it("should process and return summary rows for valid JSON with different task statuses", () => {
63+
describe("getSummaryRows", () => {
64+
it("should return correct rows for different task statuses", () => {
2465
const mockBuildSummary = JSON.stringify([
2566
{
2667
name: "Task 1",
@@ -73,19 +114,151 @@ describe("summaryGeneration", () => {
73114
]);
74115
});
75116

76-
it("writes the summary correctly", () => {
77-
const mockTableRows = [
78-
["MATLAB Task", "Status", "Description", "Duration (HH:mm:ss)"],
79-
["Test Task", "🔴 Failed", "A test task", "00:00:10"],
80-
];
81-
buildSummary.addSummary(mockTableRows);
82-
83-
expect(core.summary.addHeading).toHaveBeenCalledTimes(1);
84-
expect(core.summary.addHeading).toHaveBeenNthCalledWith(
85-
1,
86-
expect.stringContaining("MATLAB Build Results"),
117+
it("should return empty array for empty JSON array", () => {
118+
const result = buildSummary.getSummaryRows("[]");
119+
expect(result).toEqual([]);
120+
});
121+
});
122+
123+
describe("processAndAddBuildSummary", () => {
124+
it("should discover and process files matching the actionName", () => {
125+
const filePath = path.join(runnerTemp, "buildSummarymy-action_20260509_100000_001.json");
126+
fs.writeFileSync(filePath, validBuildData);
127+
128+
try {
129+
buildSummary.processAndAddBuildSummary(runnerTemp, "my-action");
130+
131+
expect(core.summary.addHeading).toHaveBeenCalledWith("MATLAB Build Results");
132+
expect(core.summary.addTable).toHaveBeenCalledTimes(1);
133+
134+
const tableArg = (core.summary.addTable as jest.Mock).mock.calls[0][0] as any[][];
135+
expect(tableArg[1]).toEqual(["compile", "🟢 Successful", "Compile source", "00:00:10"]);
136+
expect(tableArg[2]).toEqual(["test", "🔴 Failed", "Run tests", "00:00:25"]);
137+
} finally {
138+
safeDelete(filePath);
139+
}
140+
});
141+
142+
it("should ignore files for a different actionName", () => {
143+
const matchingFile = path.join(
144+
runnerTemp,
145+
"buildSummarymy-action_20260509_100000_001.json",
146+
);
147+
const nonMatchingFile = path.join(
148+
runnerTemp,
149+
"buildSummaryother-action_20260509_100000_001.json",
87150
);
88-
expect(core.summary.addTable).toHaveBeenCalledTimes(1);
89-
expect(core.summary.addTable).toHaveBeenCalledWith(mockTableRows);
151+
fs.writeFileSync(matchingFile, validBuildData);
152+
fs.writeFileSync(nonMatchingFile, validBuildData);
153+
154+
try {
155+
buildSummary.processAndAddBuildSummary(runnerTemp, "my-action");
156+
157+
expect(core.summary.addTable).toHaveBeenCalledTimes(1);
158+
expect(mockUnlinkSync).toHaveBeenCalledWith(matchingFile);
159+
expect(mockUnlinkSync).not.toHaveBeenCalledWith(nonMatchingFile);
160+
} finally {
161+
safeDelete(matchingFile);
162+
safeDelete(nonMatchingFile);
163+
}
164+
});
165+
166+
it("should not add summary when no matching files exist", () => {
167+
buildSummary.processAndAddBuildSummary(runnerTemp, "nonexistent-action");
168+
169+
expect(core.summary.addHeading).not.toHaveBeenCalled();
170+
expect(core.summary.addTable).not.toHaveBeenCalled();
171+
});
172+
173+
it("should handle non-existent directory gracefully", () => {
174+
const consoleSpy = jest.spyOn(console, "error").mockImplementation(() => {});
175+
176+
buildSummary.processAndAddBuildSummary("/nonexistent/directory/path", "my-action");
177+
178+
expect(core.summary.addTable).not.toHaveBeenCalled();
179+
expect(consoleSpy).toHaveBeenCalledWith(
180+
expect.stringContaining(
181+
"An error occurred while finding build summary file(s) in directory",
182+
),
183+
expect.any(Error),
184+
);
185+
consoleSpy.mockRestore();
186+
});
187+
188+
it("should handle invalid JSON gracefully", () => {
189+
const consoleSpy = jest.spyOn(console, "error").mockImplementation(() => {});
190+
const filePath = path.join(runnerTemp, "buildSummarymy-action_20260509_100000_002.json");
191+
fs.writeFileSync(filePath, "{ invalid json");
192+
193+
try {
194+
buildSummary.processAndAddBuildSummary(runnerTemp, "my-action");
195+
196+
expect(core.summary.addTable).not.toHaveBeenCalled();
197+
expect(consoleSpy).toHaveBeenCalledWith(
198+
"An error occurred while reading the build summary file:",
199+
expect.any(Error),
200+
);
201+
} finally {
202+
safeDelete(filePath);
203+
consoleSpy.mockRestore();
204+
}
205+
});
206+
207+
it("should delete files after processing", () => {
208+
const filePath = path.join(runnerTemp, "buildSummarymy-action_20260509_100000_003.json");
209+
fs.writeFileSync(filePath, validBuildData);
210+
211+
try {
212+
buildSummary.processAndAddBuildSummary(runnerTemp, "my-action");
213+
214+
expect(mockUnlinkSync).toHaveBeenCalledWith(filePath);
215+
} finally {
216+
safeDelete(filePath);
217+
}
218+
});
219+
220+
it("should handle file deletion errors gracefully", () => {
221+
const consoleSpy = jest.spyOn(console, "error").mockImplementation(() => {});
222+
mockUnlinkSync.mockImplementationOnce(() => {
223+
throw new Error("Permission denied");
224+
});
225+
226+
const filePath = path.join(runnerTemp, "buildSummarymy-action_20260509_100000_004.json");
227+
fs.writeFileSync(filePath, validBuildData);
228+
229+
try {
230+
buildSummary.processAndAddBuildSummary(runnerTemp, "my-action");
231+
232+
expect(core.summary.addTable).toHaveBeenCalledTimes(1);
233+
expect(consoleSpy).toHaveBeenCalledWith(
234+
expect.stringContaining(
235+
"An error occurred while trying to delete the build summary file",
236+
),
237+
expect.any(Error),
238+
);
239+
} finally {
240+
mockUnlinkSync.mockReset();
241+
safeDelete(filePath);
242+
consoleSpy.mockRestore();
243+
}
244+
});
245+
246+
it("should process multiple build summary files", () => {
247+
const file1 = path.join(runnerTemp, "buildSummarymy-action_20260509_100000_005.json");
248+
const file2 = path.join(runnerTemp, "buildSummarymy-action_20260509_100000_006.json");
249+
fs.writeFileSync(file1, validBuildData);
250+
fs.writeFileSync(file2, validBuildData);
251+
252+
try {
253+
buildSummary.processAndAddBuildSummary(runnerTemp, "my-action");
254+
255+
expect(core.summary.addHeading).toHaveBeenCalledTimes(2);
256+
expect(core.summary.addTable).toHaveBeenCalledTimes(2);
257+
expect(mockUnlinkSync).toHaveBeenCalledWith(file1);
258+
expect(mockUnlinkSync).toHaveBeenCalledWith(file2);
259+
} finally {
260+
safeDelete(file1);
261+
safeDelete(file2);
262+
}
90263
});
91264
});

0 commit comments

Comments
 (0)