diff --git a/dist/build.js b/dist/build.js index 617e320..ebafc7a 100644 --- a/dist/build.js +++ b/dist/build.js @@ -26515,6 +26515,246 @@ function splitLines(text) { return result; } +// src/outcomes.ts +var ASSUME_PENDING_CHECKS_SKIPPED_AFTER_SECS = 60; +var FailRunOn = [ + "never", + "fatal", + "error", + "warning", + "note" +]; +var OutcomeConclusion = [...FailRunOn, "success"]; +function shouldFailRun({ + failRunOn, + outcomes, + baseOutcomes +}) { + const failures = Object.entries(outcomes).flatMap(([language, outcome]) => { + const categorized = categorizeOutcome({ + outcome, + baseOutcome: baseOutcomes?.[language] + }); + if (categorized.isPending) { + return []; + } + const { severity, isRegression, description } = categorized; + const didFail = isRegression !== false && severity && OutcomeConclusion.indexOf(severity) <= OutcomeConclusion.indexOf(failRunOn); + return didFail ? [ + { + language, + reason: getReason({ + description, + isRegression + }) + } + ] : []; + }); + if (failures.length > 0) { + logger.warn("The following languages did not build successfully:"); + for (const { language, reason } of failures) { + logger.warn(` ${language}: ${reason}`); + } + return false; + } + return true; +} +function categorizeOutcome({ + outcome, + baseOutcome +}) { + const baseConclusion = baseOutcome?.commit?.conclusion; + const headConclusion = outcome.commit?.conclusion; + if (!headConclusion || baseOutcome && !baseConclusion) { + return { isPending: true }; + } + const baseChecks = baseOutcome && baseOutcome.commit?.commit ? getChecks(baseOutcome) : {}; + const headChecks = outcome.commit?.commit ? getChecks(outcome) : {}; + if ([...Object.values(headChecks), ...Object.values(baseChecks)].some( + (check) => check && check.status !== "completed" + )) { + return { isPending: true }; + } + const newDiagnostics = sortDiagnostics( + baseOutcome ? getNewDiagnostics(outcome.diagnostics, baseOutcome.diagnostics) : outcome.diagnostics + ); + const conclusions = { + fatal: [ + "fatal", + "payment_required", + "timed_out", + "upstream_merge_conflict", + "version_bump" + ], + conflict: ["merge_conflict"], + diagnostic: ["error", "warning", "note"], + success: ["success", "noop", "cancelled"] + }; + const checks = getNewChecks(headChecks, baseChecks); + const checkFailures = CheckType.filter( + (checkType) => checks[checkType] && checks[checkType].status === "completed" && ["failure", "timed_out"].includes(checks[checkType].completed.conclusion) + ); + if (headConclusion === "timed_out" || baseConclusion === "timed_out") { + return { + isPending: false, + conclusion: "timed_out", + severity: "fatal", + description: "timed out before completion", + isRegression: null + }; + } + if (conclusions.fatal.includes(headConclusion)) { + return { + isPending: false, + conclusion: "fatal", + severity: "fatal", + description: `had a "${headConclusion}" conclusion, and no code was generated`, + isRegression: baseConclusion ? conclusions.fatal.includes(baseConclusion) ? false : true : null + }; + } + if (baseConclusion && conclusions.fatal.includes(baseConclusion)) { + return { + isPending: false, + conclusion: headConclusion, + severity: null, + description: `had a "${baseOutcome?.commit?.conclusion}" conclusion in the base build, which improved to "${headConclusion}"`, + isRegression: false + }; + } + if (conclusions.diagnostic.includes(headConclusion) || newDiagnostics.length > 0 || checkFailures.length > 0) { + const categoryOutcome = conclusions.diagnostic.includes(headConclusion) ? { + severity: headConclusion, + description: `had at least one "${headConclusion}" diagnostic`, + isRegression: baseConclusion ? conclusions.success.includes(baseConclusion) || conclusions.diagnostic.indexOf(headConclusion) < conclusions.diagnostic.indexOf(baseConclusion) ? true : false : null, + rank: 1 + } : null; + const diagnosticLevelOutcome = newDiagnostics.length > 0 ? { + severity: newDiagnostics[0].level, + description: `had at least one ${baseOutcome ? "new " : ""}${newDiagnostics[0].level} diagnostic`, + isRegression: baseOutcome ? true : null, + rank: 2 + } : null; + let checkFailureOutcome; + for (const { step, severity } of [ + { step: "build", severity: "error" }, + { step: "lint", severity: "warning" }, + { step: "test", severity: "warning" } + ]) { + if (checkFailures.includes(step)) { + checkFailureOutcome = { + severity, + description: `had a failure in the ${step} CI job`, + isRegression: baseChecks ? true : null, + rank: 3 + }; + break; + } + } + const worstOutcome = [ + categoryOutcome, + diagnosticLevelOutcome, + checkFailureOutcome + ].filter((r) => r !== null).sort( + (a, b) => ( + // sort by severity then rank + conclusions.diagnostic.indexOf(a.severity) - conclusions.diagnostic.indexOf(b.severity) || a.rank - b.rank + ) + )[0]; + return { + isPending: false, + conclusion: worstOutcome.severity, + ...worstOutcome + }; + } + if (conclusions.conflict.includes(headConclusion)) { + return { + isPending: false, + conclusion: "merge_conflict", + severity: baseConclusion !== "merge_conflict" ? "warning" : null, + description: "resulted in a merge conflict between your custom code and the newly generated changes", + isRegression: baseConclusion ? baseConclusion !== "merge_conflict" ? true : false : null + }; + } + return { + isPending: false, + conclusion: headConclusion, + severity: null, + description: headConclusion === "success" ? "was successful" : `had a conclusion of ${headConclusion}`, + isRegression: null + }; +} +function getReason({ + description, + isRegression +}) { + return `Your SDK build ${description}${isRegression === true ? ", which is a regression from the base state" : isRegression === false ? ", but this did not represent a regression" : ""}.`; +} +var DiagnosticLevel = ["fatal", "error", "warning", "note"]; +function countDiagnosticLevels(diagnostics) { + return diagnostics.reduce( + (counts, diag) => { + counts[diag.level] = (counts[diag.level] || 0) + 1; + return counts; + }, + { + fatal: 0, + error: 0, + warning: 0, + note: 0 + } + ); +} +function getNewDiagnostics(diagnostics, baseDiagnostics) { + if (!baseDiagnostics) { + return diagnostics; + } + return diagnostics.filter( + (d) => !baseDiagnostics.some( + (bd) => bd.code === d.code && bd.message === d.message && bd.config_ref === d.config_ref && bd.oas_ref === d.oas_ref + ) + ); +} +function sortDiagnostics(diagnostics) { + return diagnostics.sort( + (a, b) => DiagnosticLevel.indexOf(a.level) - DiagnosticLevel.indexOf(b.level) + ); +} +var CheckType = ["build", "lint", "test"]; +function getChecks(outcome) { + const results = {}; + const commitCompletedMoreThanXSecsAgo = outcome.commit ? (/* @__PURE__ */ new Date()).getTime() - new Date(outcome.commit.completed_at).getTime() > ASSUME_PENDING_CHECKS_SKIPPED_AFTER_SECS * 1e3 : false; + for (const checkType of CheckType) { + if (outcome[checkType]?.status === "not_started" && commitCompletedMoreThanXSecsAgo) { + outcome[checkType] = { + status: "completed", + conclusion: "skipped", + completed: { + conclusion: "skipped", + url: null + }, + url: null + }; + } + results[checkType] = outcome[checkType] || null; + } + return results; +} +function getNewChecks(headChecks, baseChecks) { + const result = {}; + for (const checkType of CheckType) { + const headCheck = headChecks[checkType]; + const baseCheck = baseChecks ? baseChecks[checkType] : null; + if (headCheck) { + const baseConclusion = baseCheck?.status === "completed" && baseCheck.conclusion; + const conclusion = headCheck.status === "completed" && headCheck.conclusion; + if (!baseConclusion || baseConclusion !== conclusion) { + result[checkType] = headCheck; + } + } + } + return result; +} + // package.json var package_default = { name: "upload-openapi-spec-action", @@ -26915,7 +27155,9 @@ async function* pollBuild({ return; } const pollingStart = Date.now(); - while (Object.values(outcomes).filter(({ status }) => status === "completed").length < languages.length && Date.now() - pollingStart < maxPollingSeconds * 1e3) { + while ((Object.values(outcomes).length < languages.length || Object.values(outcomes).some( + (outcome) => categorizeOutcome({ outcome }).isPending + )) && Date.now() - pollingStart < maxPollingSeconds * 1e3) { let hasChange = false; const build2 = await stainless.builds.retrieve(buildId); for (const language of languages) { @@ -26963,7 +27205,7 @@ async function* pollBuild({ ); } const languagesWithoutOutcome = languages.filter( - (language) => !outcomes[language] || outcomes[language].commit?.status !== "completed" + (language) => !outcomes[language] || categorizeOutcome({ outcome: outcomes[language] }).isPending ); for (const language of languagesWithoutOutcome) { log.warn(`Build for ${language} timed out after ${maxPollingSeconds}s`); @@ -27136,246 +27378,6 @@ var Heading = (content) => `

${content}

`; var Link = ({ text, href }) => `${text}`; var Rule = () => `
`; -// src/outcomes.ts -var ASSUME_PENDING_CHECKS_SKIPPED_AFTER_SECS = 60; -var FailRunOn = [ - "never", - "fatal", - "error", - "warning", - "note" -]; -var OutcomeConclusion = [...FailRunOn, "success"]; -function shouldFailRun({ - failRunOn, - outcomes, - baseOutcomes -}) { - const failures = Object.entries(outcomes).flatMap(([language, outcome]) => { - const categorized = categorizeOutcome({ - outcome, - baseOutcome: baseOutcomes?.[language] - }); - if (categorized.isPending) { - return []; - } - const { severity, isRegression, description } = categorized; - const didFail = isRegression !== false && severity && OutcomeConclusion.indexOf(severity) <= OutcomeConclusion.indexOf(failRunOn); - return didFail ? [ - { - language, - reason: getReason({ - description, - isRegression - }) - } - ] : []; - }); - if (failures.length > 0) { - logger.warn("The following languages did not build successfully:"); - for (const { language, reason } of failures) { - logger.warn(` ${language}: ${reason}`); - } - return false; - } - return true; -} -function categorizeOutcome({ - outcome, - baseOutcome -}) { - const baseConclusion = baseOutcome?.commit?.conclusion; - const headConclusion = outcome.commit?.conclusion; - if (!headConclusion || baseOutcome && !baseConclusion) { - return { isPending: true }; - } - const baseChecks = baseOutcome && baseOutcome.commit?.commit ? getChecks(baseOutcome) : {}; - const headChecks = outcome.commit?.commit ? getChecks(outcome) : {}; - if ([...Object.values(headChecks), ...Object.values(baseChecks)].some( - (check) => check && check.status !== "completed" - )) { - return { isPending: true }; - } - const newDiagnostics = sortDiagnostics( - baseOutcome ? getNewDiagnostics(outcome.diagnostics, baseOutcome.diagnostics) : outcome.diagnostics - ); - const conclusions = { - fatal: [ - "fatal", - "payment_required", - "timed_out", - "upstream_merge_conflict", - "version_bump" - ], - conflict: ["merge_conflict"], - diagnostic: ["error", "warning", "note"], - success: ["success", "noop", "cancelled"] - }; - const checks = getNewChecks(headChecks, baseChecks); - const checkFailures = CheckType.filter( - (checkType) => checks[checkType] && checks[checkType].status === "completed" && ["failure", "timed_out"].includes(checks[checkType].completed.conclusion) - ); - if (headConclusion === "timed_out" || baseConclusion === "timed_out") { - return { - isPending: false, - conclusion: "timed_out", - severity: "fatal", - description: "timed out before completion", - isRegression: null - }; - } - if (conclusions.fatal.includes(headConclusion)) { - return { - isPending: false, - conclusion: "fatal", - severity: "fatal", - description: `had a "${headConclusion}" conclusion, and no code was generated`, - isRegression: baseConclusion ? conclusions.fatal.includes(baseConclusion) ? false : true : null - }; - } - if (baseConclusion && conclusions.fatal.includes(baseConclusion)) { - return { - isPending: false, - conclusion: headConclusion, - severity: null, - description: `had a "${baseOutcome?.commit?.conclusion}" conclusion in the base build, which improved to "${headConclusion}"`, - isRegression: false - }; - } - if (conclusions.diagnostic.includes(headConclusion) || newDiagnostics.length > 0 || checkFailures.length > 0) { - const categoryOutcome = conclusions.diagnostic.includes(headConclusion) ? { - severity: headConclusion, - description: `had at least one "${headConclusion}" diagnostic`, - isRegression: baseConclusion ? conclusions.success.includes(baseConclusion) || conclusions.diagnostic.indexOf(headConclusion) < conclusions.diagnostic.indexOf(baseConclusion) ? true : false : null, - rank: 1 - } : null; - const diagnosticLevelOutcome = newDiagnostics.length > 0 ? { - severity: newDiagnostics[0].level, - description: `had at least one ${baseOutcome ? "new " : ""}${newDiagnostics[0].level} diagnostic`, - isRegression: baseOutcome ? true : null, - rank: 2 - } : null; - let checkFailureOutcome; - for (const { step, severity } of [ - { step: "build", severity: "error" }, - { step: "lint", severity: "warning" }, - { step: "test", severity: "warning" } - ]) { - if (checkFailures.includes(step)) { - checkFailureOutcome = { - severity, - description: `had a failure in the ${step} CI job`, - isRegression: baseChecks ? true : null, - rank: 3 - }; - break; - } - } - const worstOutcome = [ - categoryOutcome, - diagnosticLevelOutcome, - checkFailureOutcome - ].filter((r) => r !== null).sort( - (a, b) => ( - // sort by severity then rank - conclusions.diagnostic.indexOf(a.severity) - conclusions.diagnostic.indexOf(b.severity) || a.rank - b.rank - ) - )[0]; - return { - isPending: false, - conclusion: worstOutcome.severity, - ...worstOutcome - }; - } - if (conclusions.conflict.includes(headConclusion)) { - return { - isPending: false, - conclusion: "merge_conflict", - severity: baseConclusion !== "merge_conflict" ? "warning" : null, - description: "resulted in a merge conflict between your custom code and the newly generated changes", - isRegression: baseConclusion ? baseConclusion !== "merge_conflict" ? true : false : null - }; - } - return { - isPending: false, - conclusion: headConclusion, - severity: null, - description: headConclusion === "success" ? "was successful" : `had a conclusion of ${headConclusion}`, - isRegression: null - }; -} -function getReason({ - description, - isRegression -}) { - return `Your SDK build ${description}${isRegression === true ? ", which is a regression from the base state" : isRegression === false ? ", but this did not represent a regression" : ""}.`; -} -var DiagnosticLevel = ["fatal", "error", "warning", "note"]; -function countDiagnosticLevels(diagnostics) { - return diagnostics.reduce( - (counts, diag) => { - counts[diag.level] = (counts[diag.level] || 0) + 1; - return counts; - }, - { - fatal: 0, - error: 0, - warning: 0, - note: 0 - } - ); -} -function getNewDiagnostics(diagnostics, baseDiagnostics) { - if (!baseDiagnostics) { - return diagnostics; - } - return diagnostics.filter( - (d) => !baseDiagnostics.some( - (bd) => bd.code === d.code && bd.message === d.message && bd.config_ref === d.config_ref && bd.oas_ref === d.oas_ref - ) - ); -} -function sortDiagnostics(diagnostics) { - return diagnostics.sort( - (a, b) => DiagnosticLevel.indexOf(a.level) - DiagnosticLevel.indexOf(b.level) - ); -} -var CheckType = ["build", "lint", "test"]; -function getChecks(outcome) { - const results = {}; - const commitCompletedMoreThanXSecsAgo = outcome.commit ? (/* @__PURE__ */ new Date()).getTime() - new Date(outcome.commit.completed_at).getTime() > ASSUME_PENDING_CHECKS_SKIPPED_AFTER_SECS * 1e3 : false; - for (const checkType of CheckType) { - if (outcome[checkType]?.status === "not_started" && commitCompletedMoreThanXSecsAgo) { - outcome[checkType] = { - status: "completed", - conclusion: "skipped", - completed: { - conclusion: "skipped", - url: null - }, - url: null - }; - } - results[checkType] = outcome[checkType] || null; - } - return results; -} -function getNewChecks(headChecks, baseChecks) { - const result = {}; - for (const checkType of CheckType) { - const headCheck = headChecks[checkType]; - const baseCheck = baseChecks ? baseChecks[checkType] : null; - if (headCheck) { - const baseConclusion = baseCheck?.status === "completed" && baseCheck.conclusion; - const conclusion = headCheck.status === "completed" && headCheck.conclusion; - if (!baseConclusion || baseConclusion !== conclusion) { - result[checkType] = headCheck; - } - } - } - return result; -} - // src/comment.ts var COMMENT_TITLE = Heading( `${Symbol2.HeavyAsterisk} Stainless preview builds` diff --git a/dist/merge.js b/dist/merge.js index e6c13fe..d5944b1 100644 --- a/dist/merge.js +++ b/dist/merge.js @@ -20294,7 +20294,9 @@ async function* pollBuild({ return; } const pollingStart = Date.now(); - while (Object.values(outcomes).filter(({ status }) => status === "completed").length < languages.length && Date.now() - pollingStart < maxPollingSeconds * 1e3) { + while ((Object.values(outcomes).length < languages.length || Object.values(outcomes).some( + (outcome) => categorizeOutcome({ outcome }).isPending + )) && Date.now() - pollingStart < maxPollingSeconds * 1e3) { let hasChange = false; const build2 = await stainless.builds.retrieve(buildId); for (const language of languages) { @@ -20342,7 +20344,7 @@ async function* pollBuild({ ); } const languagesWithoutOutcome = languages.filter( - (language) => !outcomes[language] || outcomes[language].commit?.status !== "completed" + (language) => !outcomes[language] || categorizeOutcome({ outcome: outcomes[language] }).isPending ); for (const language of languagesWithoutOutcome) { log.warn(`Build for ${language} timed out after ${maxPollingSeconds}s`); diff --git a/dist/preview.js b/dist/preview.js index 01e39ba..c4c1de1 100644 --- a/dist/preview.js +++ b/dist/preview.js @@ -20361,7 +20361,9 @@ async function* pollBuild({ return; } const pollingStart = Date.now(); - while (Object.values(outcomes).filter(({ status }) => status === "completed").length < languages.length && Date.now() - pollingStart < maxPollingSeconds * 1e3) { + while ((Object.values(outcomes).length < languages.length || Object.values(outcomes).some( + (outcome) => categorizeOutcome({ outcome }).isPending + )) && Date.now() - pollingStart < maxPollingSeconds * 1e3) { let hasChange = false; const build2 = await stainless.builds.retrieve(buildId); for (const language of languages) { @@ -20409,7 +20411,7 @@ async function* pollBuild({ ); } const languagesWithoutOutcome = languages.filter( - (language) => !outcomes[language] || outcomes[language].commit?.status !== "completed" + (language) => !outcomes[language] || categorizeOutcome({ outcome: outcomes[language] }).isPending ); for (const language of languagesWithoutOutcome) { log.warn(`Build for ${language} timed out after ${maxPollingSeconds}s`); diff --git a/src/runBuilds.ts b/src/runBuilds.ts index 121b90e..8cad8cd 100644 --- a/src/runBuilds.ts +++ b/src/runBuilds.ts @@ -1,7 +1,7 @@ import { createPatch, applyPatch } from "diff"; import { Stainless } from "@stainless-api/sdk"; import { logger } from "./logger"; -import type { Outcomes } from "./outcomes"; +import { categorizeOutcome, type Outcomes } from "./outcomes"; import { addBuildIdForTelemetry } from "./wrapAction"; type Build = Stainless.Builds.Build; @@ -339,8 +339,10 @@ async function* pollBuild({ const pollingStart = Date.now(); while ( - Object.values(outcomes).filter(({ status }) => status === "completed") - .length < languages.length && + (Object.values(outcomes).length < languages.length || + Object.values(outcomes).some( + (outcome) => categorizeOutcome({ outcome }).isPending, + )) && Date.now() - pollingStart < maxPollingSeconds * 1000 ) { let hasChange = false; @@ -411,7 +413,8 @@ async function* pollBuild({ const languagesWithoutOutcome = languages.filter( (language) => - !outcomes[language] || outcomes[language].commit?.status !== "completed", + !outcomes[language] || + categorizeOutcome({ outcome: outcomes[language]! }).isPending, ); for (const language of languagesWithoutOutcome) { log.warn(`Build for ${language} timed out after ${maxPollingSeconds}s`);