From 7818344f82c84709e4d7314997f8b38be2ae8aed Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Mon, 23 Mar 2026 04:40:37 +0000 Subject: [PATCH] fix(ci): harden changed extension diff fallback --- .github/workflows/ci.yml | 8 +++- scripts/test-extension.mjs | 72 ++++++++++++++++++++++++++--- test/scripts/test-extension.test.ts | 10 ++++ 3 files changed, 82 insertions(+), 8 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c036b5d82bc..cc538dd040e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -117,12 +117,18 @@ jobs: id: changed env: BASE_SHA: ${{ github.event_name == 'push' && github.event.before || github.event.pull_request.base.sha }} + BASE_REF: ${{ github.event_name == 'push' && github.ref_name || github.event.pull_request.base.ref }} run: | node --input-type=module <<'EOF' import { appendFileSync } from "node:fs"; import { listChangedExtensionIds } from "./scripts/test-extension.mjs"; - const extensionIds = listChangedExtensionIds({ base: process.env.BASE_SHA, head: "HEAD" }); + const extensionIds = listChangedExtensionIds({ + base: process.env.BASE_SHA, + head: "HEAD", + fallbackBaseRef: process.env.BASE_REF, + unavailableBaseBehavior: "all", + }); const matrix = JSON.stringify({ include: extensionIds.map((extension) => ({ extension })) }); appendFileSync(process.env.GITHUB_OUTPUT, `has_changed_extensions=${extensionIds.length > 0}\n`, "utf8"); diff --git a/scripts/test-extension.mjs b/scripts/test-extension.mjs index 4d9f7a9575e..b92d54ca70c 100644 --- a/scripts/test-extension.mjs +++ b/scripts/test-extension.mjs @@ -11,6 +11,15 @@ const __dirname = path.dirname(__filename); const repoRoot = path.resolve(__dirname, ".."); const pnpm = "pnpm"; +function runGit(args, options = {}) { + return execFileSync("git", args, { + cwd: repoRoot, + stdio: ["ignore", "pipe", "pipe"], + encoding: "utf8", + ...options, + }); +} + function normalizeRelative(inputPath) { return inputPath.split(path.sep).join("/"); } @@ -46,16 +55,53 @@ function collectTestFiles(rootPath) { return results.toSorted((left, right) => left.localeCompare(right)); } +function hasGitCommit(ref) { + if (!ref || /^0+$/.test(ref)) { + return false; + } + + try { + runGit(["rev-parse", "--verify", `${ref}^{commit}`]); + return true; + } catch { + return false; + } +} + +function resolveChangedPathsBase(params = {}) { + const base = params.base; + const head = params.head ?? "HEAD"; + const fallbackBaseRef = params.fallbackBaseRef; + + if (hasGitCommit(base)) { + return base; + } + + if (fallbackBaseRef) { + const remoteBaseRef = fallbackBaseRef.startsWith("origin/") + ? fallbackBaseRef + : `origin/${fallbackBaseRef}`; + if (hasGitCommit(remoteBaseRef)) { + const mergeBase = runGit(["merge-base", remoteBaseRef, head]).trim(); + if (hasGitCommit(mergeBase)) { + return mergeBase; + } + } + } + + if (!base) { + throw new Error("A git base revision is required to list changed extensions."); + } + + throw new Error(`Git base revision is unavailable locally: ${base}`); +} + function listChangedPaths(base, head = "HEAD") { if (!base) { throw new Error("A git base revision is required to list changed extensions."); } - return execFileSync("git", ["diff", "--name-only", base, head], { - cwd: repoRoot, - stdio: ["ignore", "pipe", "pipe"], - encoding: "utf8", - }) + return runGit(["diff", "--name-only", base, head]) .split("\n") .map((line) => line.trim()) .filter((line) => line.length > 0); @@ -107,9 +153,21 @@ export function detectChangedExtensionIds(changedPaths) { } export function listChangedExtensionIds(params = {}) { - const base = params.base; const head = params.head ?? "HEAD"; - return detectChangedExtensionIds(listChangedPaths(base, head)); + const unavailableBaseBehavior = params.unavailableBaseBehavior ?? "error"; + + try { + const base = resolveChangedPathsBase(params); + return detectChangedExtensionIds(listChangedPaths(base, head)); + } catch (error) { + if (unavailableBaseBehavior === "all") { + return listAvailableExtensionIds(); + } + if (unavailableBaseBehavior === "empty") { + return []; + } + throw error; + } } function resolveExtensionDirectory(targetArg, cwd = process.cwd()) { diff --git a/test/scripts/test-extension.test.ts b/test/scripts/test-extension.test.ts index 67adc06d2b2..6b905197062 100644 --- a/test/scripts/test-extension.test.ts +++ b/test/scripts/test-extension.test.ts @@ -4,6 +4,7 @@ import { describe, expect, it } from "vitest"; import { detectChangedExtensionIds, listAvailableExtensionIds, + listChangedExtensionIds, resolveExtensionTestPlan, } from "../../scripts/test-extension.mjs"; @@ -79,6 +80,15 @@ describe("scripts/test-extension.mjs", () => { ); }); + it("can fail safe to all extensions when the base revision is unavailable", () => { + const extensionIds = listChangedExtensionIds({ + base: "deadbeefdeadbeefdeadbeefdeadbeefdeadbeef", + unavailableBaseBehavior: "all", + }); + + expect(extensionIds).toEqual(listAvailableExtensionIds()); + }); + it("dry-run still reports a plan for extensions without tests", () => { const plan = readPlan(["copilot-proxy"]);