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
16,185 changes: 1,186 additions & 14,999 deletions dist/main/collector.bundle.js

Large diffs are not rendered by default.

408 changes: 204 additions & 204 deletions dist/main/index.bundle.js

Large diffs are not rendered by default.

20,293 changes: 3,242 additions & 17,051 deletions dist/post/index.bundle.js

Large diffs are not rendered by default.

3 changes: 1 addition & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,7 @@
"@actions/artifact": "^6.0.0",
"@actions/core": "^3.0.0",
"@actions/github": "^9.0.0",
"systeminformation": "^5.30.8",
"zod": "4.3.6"
"systeminformation": "^5.30.8"
},
"devDependencies": {
"@types/node": "^25.2.3",
Expand Down
8 changes: 0 additions & 8 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

82 changes: 39 additions & 43 deletions src/lib.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import { z } from "zod";

export const bytesPerMB: number = 1024 * 1024;
export const bytesPerGB: number = 1024 * 1024 * 1024;

Expand All @@ -9,7 +7,7 @@ export const bytesPerGB: number = 1024 * 1024 * 1024;
*/
export function getRootMountPoint(): string {
const platform = process.platform;

if (platform === 'win32') {
return 'C:';
} else if (platform === 'darwin') {
Expand All @@ -20,45 +18,43 @@ export function getRootMountPoint(): string {
}
}

export const cpuLoadPercentageSchema = z.object({
unixTimeMs: z.number(),
user: z.number().nonnegative().max(100),
system: z.number().nonnegative().max(100),
});
export const cpuLoadPercentagesSchema = z.array(cpuLoadPercentageSchema);
export const memoryUsageMBSchema = z.object({
unixTimeMs: z.number(),
used: z.number().nonnegative(),
free: z.number().nonnegative(),
});
export const memoryUsageMBsSchema = z.array(memoryUsageMBSchema);
export const diskUsageGBSchema = z.object({
unixTimeMs: z.number(),
used: z.number().nonnegative(),
available: z.number().nonnegative(),
size: z.number().nonnegative(),
});
export const diskUsageGBsSchema = z.array(diskUsageGBSchema);
export const stepMarkerSchema = z.object({
unixTimeMs: z.number(),
stepName: z.string(),
status: z.enum(["start", "end"]),
});
export const stepMarkersSchema = z.array(stepMarkerSchema);
export const metricsDataSchema = z.object({
cpuLoadPercentages: cpuLoadPercentagesSchema,
memoryUsageMBs: memoryUsageMBsSchema,
diskUsageGBs: diskUsageGBsSchema,
stepMarkers: stepMarkersSchema,
});
export interface CpuLoadPercentage {
unixTimeMs: number;
user: number;
system: number;
}

export interface MemoryUsageMB {
unixTimeMs: number;
used: number;
free: number;
}

export const alertSchema = z.object({
type: z.enum(["memory", "cpu", "disk"]),
message: z.string(),
timespan: z.number().optional(), // Single timestamp when alert occurred
timespans: z.array(z.number()).optional(), // Multiple timestamps for sustained alerts
value: z.number(),
threshold: z.number(),
});
export interface DiskUsageGB {
unixTimeMs: number;
used: number;
available: number;
size: number;
}

export type Alert = z.infer<typeof alertSchema>;
export interface StepMarker {
unixTimeMs: number;
stepName: string;
status: "start" | "end";
}

export interface MetricsData {
cpuLoadPercentages: CpuLoadPercentage[];
memoryUsageMBs: MemoryUsageMB[];
diskUsageGBs: DiskUsageGB[];
stepMarkers: StepMarker[];
}

export interface Alert {
type: "memory" | "cpu" | "disk";
message: string;
timespan?: number; // Single timestamp when alert occurred
timespans?: number[]; // Multiple timestamps for sustained alerts
value: number;
threshold: number;
}
35 changes: 17 additions & 18 deletions src/main/metrics.test.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
import { describe, it, beforeEach, mock, before, after, afterEach } from "node:test";
import * as assert from "node:assert/strict";
import type { Systeminformation } from "systeminformation";
import type { z } from "zod";
import {
type cpuLoadPercentageSchema,
metricsDataSchema,
type memoryUsageMBSchema,
type diskUsageGBSchema,
type CpuLoadPercentage,
type MetricsData,
type MemoryUsageMB,
type DiskUsageGB,
} from "../lib.ts";

describe("Metrics", () => {
Expand Down Expand Up @@ -156,7 +155,7 @@ describe("Metrics", () => {

it("should initialize with empty data arrays", () => {
const metrics = createMetrics();
const data: z.TypeOf<typeof metricsDataSchema> = JSON.parse(metrics.get());
const data: MetricsData = JSON.parse(metrics.get());

assert.ok(data.cpuLoadPercentages);
assert.ok(data.memoryUsageMBs);
Expand All @@ -177,7 +176,7 @@ describe("Metrics", () => {
await new Promise(resolve => queueMicrotask(resolve));
}

const data: z.TypeOf<typeof metricsDataSchema> = JSON.parse(metrics.get());
const data: MetricsData = JSON.parse(metrics.get());

// Verify CPU metrics are collected
assert.ok(data.cpuLoadPercentages.length > 0);
Expand Down Expand Up @@ -207,7 +206,7 @@ describe("Metrics", () => {
await new Promise(resolve => queueMicrotask(resolve));
}

const cpuData: z.TypeOf<typeof cpuLoadPercentageSchema> = JSON.parse(
const cpuData: CpuLoadPercentage = JSON.parse(
metrics.get(),
).cpuLoadPercentages[0];

Expand All @@ -227,7 +226,7 @@ describe("Metrics", () => {
await new Promise(resolve => queueMicrotask(resolve));
}

const memData: z.TypeOf<typeof memoryUsageMBSchema> = JSON.parse(
const memData: MemoryUsageMB = JSON.parse(
metrics.get(),
).memoryUsageMBs[0];

Expand All @@ -248,7 +247,7 @@ describe("Metrics", () => {
await new Promise(resolve => queueMicrotask(resolve));
}

const diskData: z.TypeOf<typeof diskUsageGBSchema> = JSON.parse(
const diskData: DiskUsageGB = JSON.parse(
metrics.get(),
).diskUsageGBs[0];

Expand All @@ -272,7 +271,7 @@ describe("Metrics", () => {
await new Promise(resolve => queueMicrotask(resolve));
}

const initialData: z.TypeOf<typeof metricsDataSchema> = JSON.parse(
const initialData: MetricsData = JSON.parse(
metrics.get(),
);
const initialCpuCount: number = initialData.cpuLoadPercentages.length;
Expand All @@ -291,7 +290,7 @@ describe("Metrics", () => {
await new Promise(resolve => queueMicrotask(resolve));
}

const updatedData: z.TypeOf<typeof metricsDataSchema> = JSON.parse(
const updatedData: MetricsData = JSON.parse(
metrics.get(),
);
const updatedCpuCount: number = updatedData.cpuLoadPercentages.length;
Expand Down Expand Up @@ -321,7 +320,7 @@ describe("Metrics", () => {
await new Promise(resolve => queueMicrotask(resolve));
}

const data: z.TypeOf<typeof metricsDataSchema> = JSON.parse(metrics.get());
const data: MetricsData = JSON.parse(metrics.get());

// Verify at least 2 data points exist
assert.ok(data.cpuLoadPercentages.length >= 2);
Expand Down Expand Up @@ -364,7 +363,7 @@ describe("Metrics", () => {
await new Promise(resolve => queueMicrotask(resolve));
}

const finalData: z.TypeOf<typeof metricsDataSchema> = JSON.parse(
const finalData: MetricsData = JSON.parse(
metrics.get(),
);

Expand Down Expand Up @@ -438,7 +437,7 @@ describe("Metrics", () => {
assert.strictEqual(writeCount, 5, "Fifth collection should write");

// Verify data is collected correctly with immediate writes
const data: z.TypeOf<typeof metricsDataSchema> = JSON.parse(metrics.get());
const data: MetricsData = JSON.parse(metrics.get());
assert.strictEqual(data.cpuLoadPercentages.length, 5, "Should have 5 CPU data points");
assert.strictEqual(data.memoryUsageMBs.length, 5, "Should have 5 memory data points");
assert.strictEqual(data.diskUsageGBs.length, 5, "Should have 5 disk data points");
Expand Down Expand Up @@ -471,7 +470,7 @@ describe("Metrics", () => {
const writtenContent = fileWrites.get(stateFilePath);
assert.ok(writtenContent, "Should have written content");

const writtenData: z.TypeOf<typeof metricsDataSchema> = JSON.parse(writtenContent);
const writtenData: MetricsData = JSON.parse(writtenContent);
assert.strictEqual(writtenData.cpuLoadPercentages.length, 1, "Written data should have 1 CPU data point");
});

Expand Down Expand Up @@ -507,7 +506,7 @@ describe("Metrics", () => {
assert.strictEqual(writeCount, 10, "Should have 10 writes after 10 collections");

// Verify all 10 data points are in memory
const data: z.TypeOf<typeof metricsDataSchema> = JSON.parse(metrics.get());
const data: MetricsData = JSON.parse(metrics.get());
assert.strictEqual(data.cpuLoadPercentages.length, 10, "Should have 10 CPU data points");
});

Expand All @@ -521,7 +520,7 @@ describe("Metrics", () => {
await new Promise(resolve => queueMicrotask(resolve));
}

const data: z.TypeOf<typeof metricsDataSchema> = JSON.parse(metrics.get());
const data: MetricsData = JSON.parse(metrics.get());

// Should have disk data from the root mount point for the current platform
assert.ok(data.diskUsageGBs.length > 0, "Should have disk data");
Expand Down
6 changes: 3 additions & 3 deletions src/main/metrics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@ import { currentLoad, mem, fsSize } from "systeminformation";
import { writeFile } from "node:fs/promises";
import { writeFileSync } from "node:fs";
import { join } from "node:path";
import type { z } from "zod";
import { metricsDataSchema, bytesPerMB, bytesPerGB, getRootMountPoint } from "../lib.ts";
import type { MetricsData } from "../lib.ts";
import { bytesPerMB, bytesPerGB, getRootMountPoint } from "../lib.ts";

export class Metrics {
private readonly data: z.TypeOf<typeof metricsDataSchema>;
private readonly data: MetricsData;
private readonly intervalMs: number;
private readonly stateFile: string;
private timeoutId: NodeJS.Timeout | null = null;
Expand Down
5 changes: 2 additions & 3 deletions src/post/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,11 @@ import { DefaultArtifactClient } from "@actions/artifact";
import { info, setFailed, summary } from "@actions/core";
import { context } from "@actions/github";
import { getMetricsData, render, collectFinalMetrics, detectAlerts } from "./lib.ts";
import type { z } from "zod";
import type { metricsDataSchema } from "../lib.ts";
import type { MetricsData } from "../lib.ts";

async function index(): Promise<void> {
const maxRetryCount: number = 10;
let metricsData: z.TypeOf<typeof metricsDataSchema>;
let metricsData: MetricsData;

// Collect one final set of metrics and get the complete data
metricsData = await collectFinalMetrics();
Expand Down
16 changes: 9 additions & 7 deletions src/post/lib.test.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
import { describe, it, before, after, mock, beforeEach } from "node:test";
import * as assert from "node:assert/strict";
import { join } from "node:path";
import type { z } from "zod";
import type { metricsDataSchema } from "../lib.js";
import type { MetricsData } from "../lib.js";

/**
* Sample metrics data for testing.
*/
const sampleMetricsData: z.TypeOf<typeof metricsDataSchema> = {
const sampleMetricsData: MetricsData = {
cpuLoadPercentages: [
{ unixTimeMs: 1704067200000, user: 25.5, system: 10.3 },
{ unixTimeMs: 1704067205000, user: 30.2, system: 12.1 },
Expand Down Expand Up @@ -165,12 +164,12 @@ describe("getMetricsData", () => {
assert.deepStrictEqual(result, sampleMetricsData);
});

it("should throw error for invalid metrics data", async () => {
it("should parse metrics data without validation", async () => {
// Compute the same path that the implementation would use
const githubStateFile = process.env.GITHUB_STATE;
const runId = process.env.GITHUB_RUN_ID || "local";
const job = process.env.GITHUB_JOB || "default";

let stateFile: string;
if (githubStateFile) {
const stateDir = join(githubStateFile, '..');
Expand All @@ -179,13 +178,16 @@ describe("getMetricsData", () => {
const runnerTemp = process.env.RUNNER_TEMP || process.env.TMPDIR || '/tmp';
stateFile = join(runnerTemp, `metrics-state-${runId}-${job}.json`);
}


// With zod removed, the function will parse any valid JSON
fileReads.set(stateFile, JSON.stringify({
cpuLoadPercentages: "not an array",
memoryUsageMBs: [],
}));

await assert.rejects(getMetricsData());
// Should not throw - just returns the parsed data as-is
const result = await getMetricsData();
assert.ok(result);
});

it("should throw error when state file doesn't exist", async () => {
Expand Down
Loading