diff --git a/dist/build.js b/dist/build.js index 3896997..55a1e12 100644 --- a/dist/build.js +++ b/dist/build.js @@ -27000,6 +27000,7 @@ async function* runBuilds({ label: "head" })) { yield { + headBuildId: build.id, baseOutcomes: null, outcomes, documentedSpec @@ -27141,6 +27142,7 @@ async function* runBuilds({ } if (lastOutcome) { yield { + headBuildId: head.id, baseOutcomes: lastBaseOutcome, outcomes: lastOutcome, documentedSpec: lastDocumentedSpec @@ -27971,6 +27973,89 @@ async function runMerge(stainless, params) { // src/preview.run.ts var fs7 = __toESM(require("node:fs")); + +// src/diffCheck.ts +async function isOnlyStatsChanged({ + stainless, + outcomes, + baseOutcomes, + headBuildId +}) { + for (const lang of Object.keys(baseOutcomes)) { + if (!(lang in outcomes)) { + return false; + } + } + for (const [lang, head] of Object.entries(outcomes)) { + if (!(lang in baseOutcomes)) { + return false; + } + const base = baseOutcomes[lang]; + const headConclusion = head.commit?.conclusion; + if (headConclusion === "noop") { + continue; + } + if (!base.commit?.completed?.commit || !head.commit?.completed?.commit) { + return false; + } + const baseSha = base.commit.completed.commit.sha; + const headSha = head.commit.completed.commit.sha; + const { owner, name } = head.commit.completed.commit.repo; + let token; + try { + const output = await stainless.builds.targetOutputs.retrieve({ + build_id: headBuildId, + target: lang, + type: "source", + output: "git" + }); + if (output.output !== "git") { + logger.debug( + `targetOutputs for ${lang} returned non-git output, skipping stats check` + ); + return false; + } + token = output.token; + } catch (e) { + logger.debug( + `Could not get git access for ${lang}, skipping stats check`, + e + ); + return false; + } + try { + const response = await fetch( + `https://api.github.com/repos/${owner}/${name}/compare/${baseSha}...${headSha}`, + { + headers: { + Authorization: `token ${token}`, + Accept: "application/vnd.github.v3+json" + } + } + ); + if (!response.ok) { + logger.debug( + `GitHub compare API returned ${response.status} for ${lang}, skipping stats check` + ); + return false; + } + const data = await response.json(); + const files = data.files ?? []; + if (!files.every((f) => f.filename === ".stats.yml")) { + return false; + } + } catch (e) { + logger.debug( + `Error comparing commits for ${lang}, skipping stats check`, + e + ); + return false; + } + } + return true; +} + +// src/preview.run.ts async function runPreview(stainless, params) { const { orgName, @@ -28143,7 +28228,7 @@ async function runPreview(stainless, params) { if (!latestRun) { throw new Error("No latest run found after build finish"); } - const { outcomes, baseOutcomes, documentedSpec } = latestRun; + const { outcomes, baseOutcomes, headBuildId, documentedSpec } = latestRun; setOutput("outcomes", outcomes); setOutput("base_outcomes", baseOutcomes); if (documentedSpec && outputDir) { @@ -28155,6 +28240,26 @@ async function runPreview(stainless, params) { if (!shouldFailRun({ failRunOn, outcomes, baseOutcomes })) { process.exit(1); } + if (makeComment && headBuildId && baseOutcomes) { + const onlyStats = await isOnlyStatsChanged({ + stainless, + outcomes, + baseOutcomes, + headBuildId + }); + if (onlyStats) { + logger.info("Only .stats.yml changed across all targets"); + if (!shouldFailRun({ failRunOn: "note", outcomes, baseOutcomes })) { + break; + } + const commentBody = printComment({ noChanges: true }); + await upsertComment(prNumber, { + body: commentBody, + skipCreate: true + }); + return; + } + } break; } } diff --git a/dist/merge.js b/dist/merge.js index a1e75de..fae761a 100644 --- a/dist/merge.js +++ b/dist/merge.js @@ -20139,6 +20139,7 @@ async function* runBuilds({ label: "head" })) { yield { + headBuildId: build.id, baseOutcomes: null, outcomes, documentedSpec @@ -20280,6 +20281,7 @@ async function* runBuilds({ } if (lastOutcome) { yield { + headBuildId: head.id, baseOutcomes: lastBaseOutcome, outcomes: lastOutcome, documentedSpec: lastDocumentedSpec diff --git a/dist/preview.js b/dist/preview.js index f701fbc..a7cd0ea 100644 --- a/dist/preview.js +++ b/dist/preview.js @@ -19222,6 +19222,87 @@ async function isConfigChanged({ return changed; } +// src/diffCheck.ts +async function isOnlyStatsChanged({ + stainless, + outcomes, + baseOutcomes, + headBuildId +}) { + for (const lang of Object.keys(baseOutcomes)) { + if (!(lang in outcomes)) { + return false; + } + } + for (const [lang, head] of Object.entries(outcomes)) { + if (!(lang in baseOutcomes)) { + return false; + } + const base = baseOutcomes[lang]; + const headConclusion = head.commit?.conclusion; + if (headConclusion === "noop") { + continue; + } + if (!base.commit?.completed?.commit || !head.commit?.completed?.commit) { + return false; + } + const baseSha = base.commit.completed.commit.sha; + const headSha = head.commit.completed.commit.sha; + const { owner, name } = head.commit.completed.commit.repo; + let token; + try { + const output = await stainless.builds.targetOutputs.retrieve({ + build_id: headBuildId, + target: lang, + type: "source", + output: "git" + }); + if (output.output !== "git") { + logger.debug( + `targetOutputs for ${lang} returned non-git output, skipping stats check` + ); + return false; + } + token = output.token; + } catch (e) { + logger.debug( + `Could not get git access for ${lang}, skipping stats check`, + e + ); + return false; + } + try { + const response = await fetch( + `https://api.github.com/repos/${owner}/${name}/compare/${baseSha}...${headSha}`, + { + headers: { + Authorization: `token ${token}`, + Accept: "application/vnd.github.v3+json" + } + } + ); + if (!response.ok) { + logger.debug( + `GitHub compare API returned ${response.status} for ${lang}, skipping stats check` + ); + return false; + } + const data = await response.json(); + const files = data.files ?? []; + if (!files.every((f) => f.filename === ".stats.yml")) { + return false; + } + } catch (e) { + logger.debug( + `Error comparing commits for ${lang}, skipping stats check`, + e + ); + return false; + } + } + return true; +} + // node_modules/.pnpm/diff@8.0.3/node_modules/diff/libesm/diff/base.js var Diff = class { diff(oldStr, newStr, options = {}) { @@ -20206,6 +20287,7 @@ async function* runBuilds({ label: "head" })) { yield { + headBuildId: build.id, baseOutcomes: null, outcomes, documentedSpec @@ -20347,6 +20429,7 @@ async function* runBuilds({ } if (lastOutcome) { yield { + headBuildId: head.id, baseOutcomes: lastBaseOutcome, outcomes: lastOutcome, documentedSpec: lastDocumentedSpec @@ -20659,7 +20742,7 @@ async function runPreview(stainless, params) { if (!latestRun) { throw new Error("No latest run found after build finish"); } - const { outcomes, baseOutcomes, documentedSpec } = latestRun; + const { outcomes, baseOutcomes, headBuildId, documentedSpec } = latestRun; setOutput("outcomes", outcomes); setOutput("base_outcomes", baseOutcomes); if (documentedSpec && outputDir) { @@ -20671,6 +20754,26 @@ async function runPreview(stainless, params) { if (!shouldFailRun({ failRunOn, outcomes, baseOutcomes })) { process.exit(1); } + if (makeComment && headBuildId && baseOutcomes) { + const onlyStats = await isOnlyStatsChanged({ + stainless, + outcomes, + baseOutcomes, + headBuildId + }); + if (onlyStats) { + logger.info("Only .stats.yml changed across all targets"); + if (!shouldFailRun({ failRunOn: "note", outcomes, baseOutcomes })) { + break; + } + const commentBody = printComment({ noChanges: true }); + await upsertComment(prNumber, { + body: commentBody, + skipCreate: true + }); + return; + } + } break; } } diff --git a/src/diffCheck.ts b/src/diffCheck.ts new file mode 100644 index 0000000..df8e41f --- /dev/null +++ b/src/diffCheck.ts @@ -0,0 +1,101 @@ +import { Stainless } from "@stainless-api/sdk"; +import { logger } from "./logger"; +import type { Outcomes } from "./outcomes"; + +export async function isOnlyStatsChanged({ + stainless, + outcomes, + baseOutcomes, + headBuildId, +}: { + stainless: Stainless; + outcomes: Outcomes; + baseOutcomes: Outcomes; + headBuildId: string; +}): Promise { + for (const lang of Object.keys(baseOutcomes)) { + if (!(lang in outcomes)) { + return false; + } + } + + for (const [lang, head] of Object.entries(outcomes)) { + if (!(lang in baseOutcomes)) { + return false; + } + const base = baseOutcomes[lang]!; + + const headConclusion = head.commit?.conclusion; + if (headConclusion === "noop") { + continue; + } + + if (!base.commit?.completed?.commit || !head.commit?.completed?.commit) { + return false; + } + + const baseSha = base.commit.completed.commit.sha; + const headSha = head.commit.completed.commit.sha; + const { owner, name } = head.commit.completed.commit.repo; + + let token: string; + try { + const output = await stainless.builds.targetOutputs.retrieve({ + build_id: headBuildId, + target: lang as Stainless.Target, + type: "source", + output: "git", + }); + if (output.output !== "git") { + logger.debug( + `targetOutputs for ${lang} returned non-git output, skipping stats check`, + ); + return false; + } + token = output.token; + } catch (e) { + logger.debug( + `Could not get git access for ${lang}, skipping stats check`, + e, + ); + return false; + } + + try { + const response = await fetch( + `https://api.github.com/repos/${owner}/${name}/compare/${baseSha}...${headSha}`, + { + headers: { + Authorization: `token ${token}`, + Accept: "application/vnd.github.v3+json", + }, + }, + ); + + if (!response.ok) { + logger.debug( + `GitHub compare API returned ${response.status} for ${lang}, skipping stats check`, + ); + return false; + } + + const data = (await response.json()) as { + status: string; + files?: Array<{ filename: string }>; + }; + + const files = data.files ?? []; + if (!files.every((f) => f.filename === ".stats.yml")) { + return false; + } + } catch (e) { + logger.debug( + `Error comparing commits for ${lang}, skipping stats check`, + e, + ); + return false; + } + } + + return true; +} diff --git a/src/preview.run.ts b/src/preview.run.ts index aa3a1d5..3567ac9 100644 --- a/src/preview.run.ts +++ b/src/preview.run.ts @@ -16,6 +16,7 @@ import { readConfig, saveConfig, } from "./config"; +import { isOnlyStatsChanged } from "./diffCheck"; import { logger } from "./logger"; import { FailRunOn, shouldFailRun } from "./outcomes"; import type { RunResult } from "./runBuilds"; @@ -272,7 +273,7 @@ export async function runPreview( throw new Error("No latest run found after build finish"); } - const { outcomes, baseOutcomes, documentedSpec } = latestRun!; + const { outcomes, baseOutcomes, headBuildId, documentedSpec } = latestRun; setOutput("outcomes", outcomes); setOutput("base_outcomes", baseOutcomes); @@ -288,6 +289,29 @@ export async function runPreview( process.exit(1); } + if (makeComment && headBuildId && baseOutcomes) { + const onlyStats = await isOnlyStatsChanged({ + stainless, + outcomes, + baseOutcomes, + headBuildId, + }); + + if (onlyStats) { + logger.info("Only .stats.yml changed across all targets"); + + if (!shouldFailRun({ failRunOn: "note", outcomes, baseOutcomes })) { + break; + } + const commentBody = printComment({ noChanges: true }); + await upsertComment(prNumber, { + body: commentBody, + skipCreate: true, + }); + return; + } + } + break; } } diff --git a/src/runBuilds.ts b/src/runBuilds.ts index 32e0856..51ff28e 100644 --- a/src/runBuilds.ts +++ b/src/runBuilds.ts @@ -10,6 +10,7 @@ const POLLING_INTERVAL_SECONDS = 5; const MAX_POLLING_SECONDS = 10 * 60; // 10 minutes export type RunResult = { + headBuildId: string | null; baseOutcomes: Outcomes | null; outcomes: Outcomes; documentedSpec: string | null; @@ -95,6 +96,7 @@ export async function* runBuilds({ label: "head", })) { yield { + headBuildId: build.id, baseOutcomes: null, outcomes, documentedSpec, @@ -265,6 +267,7 @@ export async function* runBuilds({ if (lastOutcome) { yield { + headBuildId: head.id, baseOutcomes: lastBaseOutcome, outcomes: lastOutcome, documentedSpec: lastDocumentedSpec,