diff --git a/CHANGELOG.md b/CHANGELOG.md
index a5f484c25..c95986f01 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,7 @@
## Unreleased
+- **fix** improve parsing for custom benchmarks (#323)
+
# [v1.20.5](https://github.com/benchmark-action/github-action-benchmark/releases/tag/v1.20.5) - 02 Sep 2025
- **feat** allow to parse generic cargo bench/criterion units (#280)
diff --git a/package-lock.json b/package-lock.json
index b027b8187..f9b9f3acd 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -12,7 +12,8 @@
"@actions/core": "^1.10.0",
"@actions/exec": "^1.1.1",
"@actions/github": "^5.1.1",
- "@actions/io": "^1.1.2"
+ "@actions/io": "^1.1.2",
+ "zod": "^4.1.5"
},
"devDependencies": {
"@types/acorn": "^4.0.5",
@@ -21,7 +22,7 @@
"@types/deep-equal": "^1.0.1",
"@types/jest": "^29.5.11",
"@types/markdown-it": "^12.2.3",
- "@types/node": "^13.9.1",
+ "@types/node": "^24.3.1",
"@types/rimraf": "^2.0.3",
"@typescript-eslint/eslint-plugin": "^5.4.0",
"@typescript-eslint/parser": "^5.4.0",
@@ -39,7 +40,7 @@
"prettier": "^2.4.1",
"rimraf": "^3.0.2",
"ts-jest": "^29.1.2",
- "typescript": "^4.5.2"
+ "typescript": "^5.9.2"
}
},
"node_modules/@actions/core": {
@@ -1507,10 +1508,13 @@
"dev": true
},
"node_modules/@types/node": {
- "version": "13.9.1",
- "resolved": "https://registry.npmjs.org/@types/node/-/node-13.9.1.tgz",
- "integrity": "sha512-E6M6N0blf/jiZx8Q3nb0vNaswQeEyn0XlupO+xN6DtJ6r6IT4nXrTry7zhIfYvFCl3/8Cu6WIysmUBKiqV0bqQ==",
- "dev": true
+ "version": "24.3.1",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-24.3.1.tgz",
+ "integrity": "sha512-3vXmQDXy+woz+gnrTvuvNrPzekOi+Ds0ReMxw0LzBiK3a+1k0kQn9f2NWk+lgD4rJehFUmYy2gMhJ2ZI+7YP9g==",
+ "dev": true,
+ "dependencies": {
+ "undici-types": "~7.10.0"
+ }
},
"node_modules/@types/rimraf": {
"version": "2.0.3",
@@ -5795,16 +5799,16 @@
}
},
"node_modules/typescript": {
- "version": "4.5.2",
- "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.5.2.tgz",
- "integrity": "sha512-5BlMof9H1yGt0P8/WF+wPNw6GfctgGjXp5hkblpyT+8rkASSmkUKMXrxR0Xg8ThVCi/JnHQiKXeBaEwCeQwMFw==",
+ "version": "5.9.2",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz",
+ "integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==",
"dev": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
- "node": ">=4.2.0"
+ "node": ">=14.17"
}
},
"node_modules/uc.micro": {
@@ -5813,6 +5817,12 @@
"integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==",
"dev": true
},
+ "node_modules/undici-types": {
+ "version": "7.10.0",
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.10.0.tgz",
+ "integrity": "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag==",
+ "dev": true
+ },
"node_modules/universal-user-agent": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.0.tgz",
@@ -6034,6 +6044,14 @@
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
+ },
+ "node_modules/zod": {
+ "version": "4.1.5",
+ "resolved": "https://registry.npmjs.org/zod/-/zod-4.1.5.tgz",
+ "integrity": "sha512-rcUUZqlLJgBC33IT3PNMgsCq6TzLQEG/Ei/KTCU0PedSWRMAXoOUN+4t/0H+Q8bdnLPdqUYnvboJT0bn/229qg==",
+ "funding": {
+ "url": "https://github.com/sponsors/colinhacks"
+ }
}
},
"dependencies": {
@@ -7247,10 +7265,13 @@
"dev": true
},
"@types/node": {
- "version": "13.9.1",
- "resolved": "https://registry.npmjs.org/@types/node/-/node-13.9.1.tgz",
- "integrity": "sha512-E6M6N0blf/jiZx8Q3nb0vNaswQeEyn0XlupO+xN6DtJ6r6IT4nXrTry7zhIfYvFCl3/8Cu6WIysmUBKiqV0bqQ==",
- "dev": true
+ "version": "24.3.1",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-24.3.1.tgz",
+ "integrity": "sha512-3vXmQDXy+woz+gnrTvuvNrPzekOi+Ds0ReMxw0LzBiK3a+1k0kQn9f2NWk+lgD4rJehFUmYy2gMhJ2ZI+7YP9g==",
+ "dev": true,
+ "requires": {
+ "undici-types": "~7.10.0"
+ }
},
"@types/rimraf": {
"version": "2.0.3",
@@ -10326,9 +10347,9 @@
"dev": true
},
"typescript": {
- "version": "4.5.2",
- "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.5.2.tgz",
- "integrity": "sha512-5BlMof9H1yGt0P8/WF+wPNw6GfctgGjXp5hkblpyT+8rkASSmkUKMXrxR0Xg8ThVCi/JnHQiKXeBaEwCeQwMFw==",
+ "version": "5.9.2",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz",
+ "integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==",
"dev": true
},
"uc.micro": {
@@ -10337,6 +10358,12 @@
"integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==",
"dev": true
},
+ "undici-types": {
+ "version": "7.10.0",
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.10.0.tgz",
+ "integrity": "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag==",
+ "dev": true
+ },
"universal-user-agent": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.0.tgz",
@@ -10496,6 +10523,11 @@
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
"integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
"dev": true
+ },
+ "zod": {
+ "version": "4.1.5",
+ "resolved": "https://registry.npmjs.org/zod/-/zod-4.1.5.tgz",
+ "integrity": "sha512-rcUUZqlLJgBC33IT3PNMgsCq6TzLQEG/Ei/KTCU0PedSWRMAXoOUN+4t/0H+Q8bdnLPdqUYnvboJT0bn/229qg=="
}
}
}
diff --git a/package.json b/package.json
index faa2b4aa4..a2c9472cb 100644
--- a/package.json
+++ b/package.json
@@ -34,7 +34,8 @@
"@actions/core": "^1.10.0",
"@actions/exec": "^1.1.1",
"@actions/github": "^5.1.1",
- "@actions/io": "^1.1.2"
+ "@actions/io": "^1.1.2",
+ "zod": "^4.1.5"
},
"devDependencies": {
"@types/acorn": "^4.0.5",
@@ -43,7 +44,7 @@
"@types/deep-equal": "^1.0.1",
"@types/jest": "^29.5.11",
"@types/markdown-it": "^12.2.3",
- "@types/node": "^13.9.1",
+ "@types/node": "^24.3.1",
"@types/rimraf": "^2.0.3",
"@typescript-eslint/eslint-plugin": "^5.4.0",
"@typescript-eslint/parser": "^5.4.0",
@@ -61,6 +62,6 @@
"prettier": "^2.4.1",
"rimraf": "^3.0.2",
"ts-jest": "^29.1.2",
- "typescript": "^4.5.2"
+ "typescript": "^5.9.2"
}
}
diff --git a/src/extract.ts b/src/extract.ts
index ec7bc35e2..60736e61c 100644
--- a/src/extract.ts
+++ b/src/extract.ts
@@ -2,14 +2,19 @@
import { promises as fs } from 'fs';
import * as github from '@actions/github';
import { Config, ToolType } from './config';
+import { z } from 'zod';
-export interface BenchmarkResult {
- name: string;
- value: number;
- range?: string;
- unit: string;
- extra?: string;
-}
+export const BenchmarkResult = z.object({
+ name: z.coerce.string(),
+ value: z.coerce.number(),
+ range: z.coerce.string().optional(),
+ unit: z.coerce.string(),
+ extra: z.coerce.string().optional(),
+});
+
+export type BenchmarkResult = z.infer;
+
+export const BenchmarkResults = z.array(BenchmarkResult);
interface GitHubUser {
email?: string;
@@ -658,13 +663,11 @@ function extractBenchmarkDotnetResult(output: string): BenchmarkResult[] {
function extractCustomBenchmarkResult(output: string): BenchmarkResult[] {
try {
- const json: BenchmarkResult[] = JSON.parse(output);
- return json.map(({ name, value, unit, range, extra }) => {
- return { name, value, unit, range, extra };
- });
- } catch (err: any) {
+ return BenchmarkResults.parse(JSON.parse(output));
+ } catch (err: unknown) {
+ const errMessage = err instanceof Error ? err.message : String(err);
throw new Error(
- `Output file for 'custom-(bigger|smaller)-is-better' must be JSON file containing an array of entries in BenchmarkResult format: ${err.message}`,
+ `Output file for 'custom-(bigger|smaller)-is-better' must be JSON file containing an array of entries in BenchmarkResult format: ${errMessage}`,
);
}
}
@@ -673,7 +676,6 @@ function extractLuauBenchmarkResult(output: string): BenchmarkResult[] {
const lines = output.split(/\n/);
const results: BenchmarkResult[] = [];
- output;
for (const line of lines) {
if (!line.startsWith('SUCCESS')) continue;
const [_0, name, _2, valueStr, _4, range, _6, extra] = line.split(/\s+/);
diff --git a/test/__snapshots__/extract.spec.ts.snap b/test/__snapshots__/extract.spec.ts.snap
index 76d1e91a9..720bfa5f5 100644
--- a/test/__snapshots__/extract.spec.ts.snap
+++ b/test/__snapshots__/extract.spec.ts.snap
@@ -427,9 +427,7 @@ exports[`extractResult() extracts benchmark output from customBiggerIsBetter - c
{
"benches": [
{
- "extra": undefined,
"name": "My Custom Bigger Is Better Benchmark - Throughput",
- "range": undefined,
"unit": "req/s",
"value": 70,
},
@@ -467,9 +465,7 @@ exports[`extractResult() extracts benchmark output from customSmallerIsBetter -
"value": 50,
},
{
- "extra": undefined,
"name": "My Custom Smaller Is Better Benchmark - Memory Used",
- "range": undefined,
"unit": "Megabytes",
"value": 100,
},
@@ -487,6 +483,47 @@ exports[`extractResult() extracts benchmark output from customSmallerIsBetter -
}
`;
+exports[`extractResult() extracts benchmark output from customSmallerIsBetter - customSmallerIsBetter_output2.json 1`] = `
+{
+ "benches": [
+ {
+ "name": "No instrumentation",
+ "range": "0.519165",
+ "unit": "ns",
+ "value": 90.9439,
+ },
+ {
+ "name": "Deactivated probe",
+ "range": "8.21371",
+ "unit": "ns",
+ "value": 445.661,
+ },
+ {
+ "name": "No logging",
+ "range": "10.2008",
+ "unit": "ns",
+ "value": 1847.38,
+ },
+ {
+ "name": "Binary file",
+ "range": "87.1657",
+ "unit": "ns",
+ "value": 3886.75,
+ },
+ ],
+ "commit": {
+ "author": null,
+ "committer": null,
+ "id": "123456789abcdef",
+ "message": "this is dummy",
+ "timestamp": "dummy timestamp",
+ "url": "https://github.com/dummy/repo",
+ },
+ "date": 1712131503296,
+ "tool": "customSmallerIsBetter",
+}
+`;
+
exports[`extractResult() extracts benchmark output from go - go_fiber_output.txt 1`] = `
{
"benches": [
diff --git a/test/data/extract/customSmallerIsBetter_output2.json b/test/data/extract/customSmallerIsBetter_output2.json
new file mode 100644
index 000000000..dad69075f
--- /dev/null
+++ b/test/data/extract/customSmallerIsBetter_output2.json
@@ -0,0 +1,26 @@
+[
+ {
+ "name": "No instrumentation",
+ "unit": "ns",
+ "value": 90.9439,
+ "range": 0.519165
+ },
+ {
+ "name": "Deactivated probe",
+ "unit": "ns",
+ "value": 445.661,
+ "range": 8.21371
+ },
+ {
+ "name": "No logging",
+ "unit": "ns",
+ "value": 1847.38,
+ "range": 10.2008
+ },
+ {
+ "name": "Binary file",
+ "unit": "ns",
+ "value": 3886.75,
+ "range": 87.1657
+ }
+]
diff --git a/test/extract.spec.ts b/test/extract.spec.ts
index 48a5c271a..f07a81d53 100644
--- a/test/extract.spec.ts
+++ b/test/extract.spec.ts
@@ -143,6 +143,10 @@ describe('extractResult()', function () {
tool: 'customSmallerIsBetter',
file: 'customSmallerIsBetter_output.json',
},
+ {
+ tool: 'customSmallerIsBetter',
+ file: 'customSmallerIsBetter_output2.json',
+ },
];
it.each(normalCases)(`extracts benchmark output from $tool - $file`, async function (test) {