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({}); + }); });