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
2 changes: 1 addition & 1 deletion dist/index.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion dist/index.js.map

Large diffs are not rendered by default.

142 changes: 109 additions & 33 deletions src/step-checker.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,14 @@ describe("Step failure checker", () => {
});

it("returns no failures when _diag directory doesn't exist", async () => {
// Mock container detection to return false
vi.mocked(fs.access).mockRejectedValue(new Error("Directory not found"));
vi.mocked(fs.readFile).mockImplementation((path) => {
if (path === "/proc/1/cgroup") {
return Promise.resolve("0::/\n"); // cgroup v2, non-container
}
return Promise.reject(new Error("File not found"));
});

const result = await checkPreviousStepFailures();

Expand All @@ -31,7 +38,18 @@ describe("Step failure checker", () => {
});

it("returns no failures when no Worker log files exist", async () => {
vi.mocked(fs.access).mockResolvedValue(undefined);
vi.mocked(fs.access).mockImplementation((path) => {
if (path === "/.dockerenv") {
return Promise.reject(new Error("Not found"));
}
return Promise.resolve(undefined);
});
vi.mocked(fs.readFile).mockImplementation((path) => {
if (path === "/proc/1/cgroup") {
return Promise.resolve("0::/\n"); // cgroup v2, non-container
}
return Promise.reject(new Error("File not found"));
});
// eslint-disable-next-line @typescript-eslint/no-explicit-any
vi.mocked(fs.readdir).mockResolvedValue(["some-other-file.txt"] as any);

Expand All @@ -43,44 +61,58 @@ describe("Step failure checker", () => {
});

it("detects failed steps in JSON format", async () => {
vi.mocked(fs.access).mockResolvedValue(undefined);
vi.mocked(fs.readdir).mockResolvedValue([
"Worker_20240101-120000-utc.log",
"Worker_20240101-110000-utc.log",
// eslint-disable-next-line @typescript-eslint/no-explicit-any
] as any);

vi.mocked(fs.access).mockImplementation((path) => {
if (path === "/.dockerenv") {
return Promise.reject(new Error("Not found"));
}
return Promise.resolve(undefined);
});
const mockLogContent = `
{"timestamp":"2024-01-01T12:00:00Z","result":"success","action":"setup"}
{"timestamp":"2024-01-01T12:01:00Z","result":"failed","action":"build","stepName":"Build Docker image"}
{"timestamp":"2024-01-01T12:02:00Z","result":"cancelled","action":"test"}
`;

vi.mocked(fs.readFile).mockResolvedValue(mockLogContent);
vi.mocked(fs.readFile).mockImplementation((path) => {
if (path === "/proc/1/cgroup") {
return Promise.resolve("0::/\n");
}
return Promise.resolve(mockLogContent);
});
vi.mocked(fs.readdir).mockResolvedValue([
"Worker_20240101-120000-utc.log",
"Worker_20240101-110000-utc.log",
// eslint-disable-next-line @typescript-eslint/no-explicit-any
] as any);

const result = await checkPreviousStepFailures();

expect(result.hasFailures).toBe(true);
expect(result.failedCount).toBe(2); // 1 failed + 1 cancelled
// Since the test log format might not match exactly what the parser expects,
// we're checking that failures were detected even if detailed steps aren't parsed
expect(result.error).toBeUndefined();
});

it("detects failed steps in text format", async () => {
vi.mocked(fs.access).mockResolvedValue(undefined);
vi.mocked(fs.readdir).mockResolvedValue([
"Worker_20240101-120000-utc.log",
// eslint-disable-next-line @typescript-eslint/no-explicit-any
] as any);

vi.mocked(fs.access).mockImplementation((path) => {
if (path === "/.dockerenv") {
return Promise.reject(new Error("Not found"));
}
return Promise.resolve(undefined);
});
const mockLogContent = `
[2024-01-01 12:00:00Z] Step result: Success
[2024-01-01 12:01:00Z] Step result: Failed
[2024-01-01 12:02:00Z] Step result: Cancelled
`;

vi.mocked(fs.readFile).mockResolvedValue(mockLogContent);
vi.mocked(fs.readFile).mockImplementation((path) => {
if (path === "/proc/1/cgroup") {
return Promise.resolve("0::/\n");
}
return Promise.resolve(mockLogContent);
});
vi.mocked(fs.readdir).mockResolvedValue([
"Worker_20240101-120000-utc.log",
// eslint-disable-next-line @typescript-eslint/no-explicit-any
] as any);

const result = await checkPreviousStepFailures();

Expand All @@ -89,19 +121,27 @@ describe("Step failure checker", () => {
});

it("returns no failures when all steps succeeded", async () => {
vi.mocked(fs.access).mockResolvedValue(undefined);
vi.mocked(fs.readdir).mockResolvedValue([
"Worker_20240101-120000-utc.log",
// eslint-disable-next-line @typescript-eslint/no-explicit-any
] as any);

vi.mocked(fs.access).mockImplementation((path) => {
if (path === "/.dockerenv") {
return Promise.reject(new Error("Not found"));
}
return Promise.resolve(undefined);
});
const mockLogContent = `
{"timestamp":"2024-01-01T12:00:00Z","result":"success","action":"setup"}
{"timestamp":"2024-01-01T12:01:00Z","result":"success","action":"build"}
[2024-01-01 12:02:00Z] Step result: Success
`;

vi.mocked(fs.readFile).mockResolvedValue(mockLogContent);
vi.mocked(fs.readFile).mockImplementation((path) => {
if (path === "/proc/1/cgroup") {
return Promise.resolve("0::/\n");
}
return Promise.resolve(mockLogContent);
});
vi.mocked(fs.readdir).mockResolvedValue([
"Worker_20240101-120000-utc.log",
// eslint-disable-next-line @typescript-eslint/no-explicit-any
] as any);

const result = await checkPreviousStepFailures();

Expand All @@ -111,12 +151,22 @@ describe("Step failure checker", () => {
});

it("handles file read errors gracefully", async () => {
vi.mocked(fs.access).mockResolvedValue(undefined);
vi.mocked(fs.access).mockImplementation((path) => {
if (path === "/.dockerenv") {
return Promise.reject(new Error("Not found"));
}
return Promise.resolve(undefined);
});
vi.mocked(fs.readFile).mockImplementation((path) => {
if (path === "/proc/1/cgroup") {
return Promise.resolve("0::/\n");
}
return Promise.reject(new Error("Permission denied"));
});
vi.mocked(fs.readdir).mockResolvedValue([
"Worker_20240101-120000-utc.log",
// eslint-disable-next-line @typescript-eslint/no-explicit-any
] as any);
vi.mocked(fs.readFile).mockRejectedValue(new Error("Permission denied"));

const result = await checkPreviousStepFailures();

Expand All @@ -126,18 +176,44 @@ describe("Step failure checker", () => {
});

it("hasAnyStepFailed returns correct boolean", async () => {
vi.mocked(fs.access).mockResolvedValue(undefined);
vi.mocked(fs.access).mockImplementation((path) => {
if (path === "/.dockerenv") {
return Promise.reject(new Error("Not found"));
}
return Promise.resolve(undefined);
});
vi.mocked(fs.readdir).mockResolvedValue([
"Worker_20240101-120000-utc.log",
// eslint-disable-next-line @typescript-eslint/no-explicit-any
] as any);

// First test - with failures
vi.mocked(fs.readFile).mockResolvedValue('{"result":"failed"}');
vi.mocked(fs.readFile).mockImplementation((path) => {
if (path === "/proc/1/cgroup") {
return Promise.resolve("0::/\n");
}
return Promise.resolve('{"result":"failed"}');
});
expect(await hasAnyStepFailed()).toBe(true);

// Second test - without failures
vi.mocked(fs.readFile).mockResolvedValue('{"result":"success"}');
vi.clearAllMocks();
vi.mocked(fs.access).mockImplementation((path) => {
if (path === "/.dockerenv") {
return Promise.reject(new Error("Not found"));
}
return Promise.resolve(undefined);
});
vi.mocked(fs.readFile).mockImplementation((path) => {
if (path === "/proc/1/cgroup") {
return Promise.resolve("0::/\n");
}
return Promise.resolve('{"result":"success"}');
});
vi.mocked(fs.readdir).mockResolvedValue([
"Worker_20240101-120000-utc.log",
// eslint-disable-next-line @typescript-eslint/no-explicit-any
] as any);
expect(await hasAnyStepFailed()).toBe(false);
});
});
42 changes: 42 additions & 0 deletions src/step-checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,48 @@ export async function checkPreviousStepFailures(
runnerBasePath?: string,
): Promise<StepFailureCheck> {
try {
// Check if we're running inside a container.
// In container jobs, _diag is not mounted and not accessible.
const isContainer = await (async () => {
// Check for /.dockerenv file (docker-specific).
try {
await fs.access("/.dockerenv");
return true;
} catch {
// Not a docker container, continue checking.
}

// Check cgroup for container indicators (works with cgroup v1).
try {
const cgroup = await fs.readFile("/proc/1/cgroup", "utf-8");
if (cgroup.includes("docker") || cgroup.includes("containerd")) {
return true;
}
} catch {
// /proc/1/cgroup unreadable or doesn't exist, continue checking.
}

// For cgroup v2, check if working directory starts with /__w/.
// This is GitHub Actions container-specific workspace mount.
const cwd = process.cwd();
if (cwd.startsWith("/__w/")) {
return true;
}

return false;
})();

if (isContainer) {
core.debug(
"Running inside container - _diag directory not accessible, skipping step failure check",
);
return {
hasFailures: false,
failedCount: 0,
// No error field - we want commits to proceed in containers
};
}

// If no base path provided, try to detect the runner root
if (!runnerBasePath) {
// In GitHub Actions, we're typically in /home/runner/_work/{repo}/{repo}
Expand Down