From 9933948b196f3a7ee379bc3a941dbb36b60f6441 Mon Sep 17 00:00:00 2001
From: "github-actions[bot]"
<41898282+github-actions[bot]@users.noreply.github.com>
Date: Sat, 17 Oct 2026 03:59:30 +0000
Subject: [PATCH] [jsweep] Clean workflow_job_durations.cjs
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---
actions/setup/js/workflow_job_durations.cjs | 21 ++++++-----
.../setup/js/workflow_job_durations.test.cjs | 37 +++++++++++++++++++
2 files changed, 48 insertions(+), 10 deletions(-)
diff --git a/actions/setup/js/workflow_job_durations.cjs b/actions/setup/js/workflow_job_durations.cjs
index 2691985f441..b4802a58203 100644
--- a/actions/setup/js/workflow_job_durations.cjs
+++ b/actions/setup/js/workflow_job_durations.cjs
@@ -1,6 +1,11 @@
// @ts-check
///
+const JOB_ID_PATTERN = /^[A-Za-z_][A-Za-z0-9_-]*$/;
+
+/** @param {unknown} value */
+const isPlainObject = value => !!value && typeof value === "object" && !Array.isArray(value);
+
/**
* Only uniquely matched completed jobs can supply a dependency's duration.
* Matrix and reusable-workflow groups do not have a single job duration.
@@ -13,13 +18,7 @@ async function collectJobDurations({ githubClient = global.github, runContext =
if (env.GH_AW_JOB_DURATIONS) {
try {
const cached = JSON.parse(env.GH_AW_JOB_DURATIONS);
- if (
- cached &&
- typeof cached === "object" &&
- !Array.isArray(cached) &&
- Object.entries(cached).every(([id, duration]) => /^[A-Za-z_][A-Za-z0-9_-]*$/.test(id) && typeof duration === "number" && Number.isSafeInteger(duration) && duration >= 0)
- )
- return cached;
+ if (isPlainObject(cached) && Object.entries(cached).every(([id, duration]) => JOB_ID_PATTERN.test(id) && typeof duration === "number" && Number.isSafeInteger(duration) && duration >= 0)) return cached;
warn("Unified session: invalid cached job durations; reading Actions job timing");
} catch (error) {
if (!(error instanceof SyntaxError)) throw error;
@@ -38,7 +37,7 @@ async function collectJobDurations({ githubClient = global.github, runContext =
warn("Unified session: job durations unavailable (invalid job metadata)");
return {};
}
- if (!needs || typeof needs !== "object" || Array.isArray(needs) || !names || typeof names !== "object" || Array.isArray(names) || !eligible || typeof eligible !== "object" || Array.isArray(eligible)) {
+ if (!isPlainObject(needs) || !isPlainObject(names) || !isPlainObject(eligible)) {
warn("Unified session: job durations unavailable (invalid job metadata)");
return {};
}
@@ -62,7 +61,7 @@ async function collectJobDurations({ githubClient = global.github, runContext =
jobs.push(...response.data.jobs);
if (response.data.jobs.length < 100) break;
}
- } catch (error) {
+ } catch {
// API errors can include request details; never log the response or metadata.
warn("Unified session: job durations unavailable (Actions API request failed)");
return {};
@@ -70,10 +69,12 @@ async function collectJobDurations({ githubClient = global.github, runContext =
/** @type {Record} */
const durations = Object.create(null);
const knownIds = [...new Set([...Object.keys(needs), ...Object.keys(names)])];
+ /** @param {string} id */
const displayName = id => (typeof names[id] === "string" && names[id] ? names[id] : id);
+ /** @param {string} name @param {string} display */
const matchesName = (name, display) => name === display || name.startsWith(`${display} (`) || name.startsWith(`${display} / `);
for (const [jobId, dependency] of Object.entries(needs)) {
- if (eligible[jobId] !== true || !/^[A-Za-z_][A-Za-z0-9_-]*$/.test(jobId) || !["success", "failure", "cancelled"].includes(dependency?.result)) continue;
+ if (eligible[jobId] !== true || !JOB_ID_PATTERN.test(jobId) || !["success", "failure", "cancelled"].includes(dependency?.result)) continue;
const matches = jobs.filter(job => job.name === displayName(jobId) && (job.run_attempt === undefined || job.run_attempt === attempt));
const ambiguous = knownIds.some(id => id !== jobId && matchesName(displayName(jobId), displayName(id)));
if (matches.length !== 1 || ambiguous) {
diff --git a/actions/setup/js/workflow_job_durations.test.cjs b/actions/setup/js/workflow_job_durations.test.cjs
index a34d04c02bd..d54e65d913d 100644
--- a/actions/setup/js/workflow_job_durations.test.cjs
+++ b/actions/setup/js/workflow_job_durations.test.cjs
@@ -158,4 +158,41 @@ describe("workflow job durations", () => {
expect(options.githubClient.rest.actions.listJobsForWorkflowRunAttempt).not.toHaveBeenCalled();
});
});
+
+ it("warns and falls back to the API when cached durations are invalid", async () => {
+ for (const cached of ['{"agent":-1}', '{"bad id":1}', "[1]", "null", "not json"]) {
+ const options = fixture();
+ options.env.GH_AW_JOB_DURATIONS = cached;
+ expect(await collectJobDurations(options)).toEqual({ agent: 2000 });
+ expect(options.warn).toHaveBeenCalledWith("Unified session: invalid cached job durations; reading Actions job timing");
+ }
+ });
+
+ it("returns valid cached durations without an API request", async () => {
+ const options = fixture();
+ options.env.GH_AW_JOB_DURATIONS = '{"agent":5}';
+ expect(await collectJobDurations(options)).toEqual({ agent: 5 });
+ expect(options.githubClient.rest.actions.listJobsForWorkflowRunAttempt).not.toHaveBeenCalled();
+ });
+
+ it("rejects non-object job metadata", async () => {
+ for (const key of ["GH_AW_JOB_RESULTS", "GH_AW_JOB_DISPLAY_NAMES", "GH_AW_JOB_TIMING_ELIGIBLE"]) {
+ const options = fixture();
+ options.env[key] = "[]";
+ expect(await collectJobDurations(options)).toEqual({});
+ expect(options.warn).toHaveBeenCalledWith("Unified session: job durations unavailable (invalid job metadata)");
+ }
+ });
+
+ it("warns when the run context or attempt is missing", async () => {
+ const options = fixture();
+ options.env.GITHUB_RUN_ATTEMPT = "0";
+ expect(await collectJobDurations(options)).toEqual({});
+ expect(options.warn).toHaveBeenCalledWith("Unified session: job durations unavailable (missing Actions run context)");
+ });
+
+ it("skips jobs with invalid IDs or non-terminal results", async () => {
+ const options = fixture({ "bad id": { result: "success" }, agent: { result: "skipped" } }, { agent: "Agent" });
+ expect(await collectJobDurations(options)).toEqual({});
+ });
});