diff --git a/public/scripts/main.js b/public/scripts/main.js index 215d6e6..80fe340 100644 --- a/public/scripts/main.js +++ b/public/scripts/main.js @@ -1,23 +1,16 @@ /* global bootstrap */ -import { parseRepositoryURL, generateWorkflow } from "./workflow.js"; +import { + parseRepositoryURL, + detectDefaultBranch, + generateWorkflow, +} from "./workflow.js"; function navigateTo(url) { window.open(url, "_blank"); } window.navigateTo = navigateTo; -function generateWorkflowWithFormInputs() { - return generateWorkflow({ - useBatchToken: document.getElementById("use-batch-token").checked, - useVirtualDisplay: document.getElementById("use-virtual-display").checked, - buildAcrossPlatforms: document.getElementById("build-across-platforms") - .checked, - siteUrl: - window.location.origin + window.location.pathname.replace(/\/[^/]*$/, ""), - }); -} - -function handleFormSubmit(e) { +async function handleFormSubmit(e) { e.preventDefault(); const repoField = document.getElementById("repo"); @@ -28,7 +21,18 @@ function handleFormSubmit(e) { } repoField.classList.remove("is-invalid"); - const workflow = generateWorkflowWithFormInputs(); + let branch = await detectDefaultBranch(repoInfo); + if (!branch) branch = "main"; + + const workflow = generateWorkflow({ + useBatchToken: document.getElementById("use-batch-token").checked, + useVirtualDisplay: document.getElementById("use-virtual-display").checked, + buildAcrossPlatforms: document.getElementById("build-across-platforms") + .checked, + siteUrl: + window.location.origin + window.location.pathname.replace(/\/[^/]*$/, ""), + branch, + }); const encoded = encodeURIComponent(workflow); const filePath = ".github/workflows/matlab.yml"; @@ -37,7 +41,7 @@ function handleFormSubmit(e) { if (repoInfo.enterprise) { url += `/enterprises/${repoInfo.enterprise}`; } - url += `/${repoInfo.owner}/${repoInfo.repo}/new/main?filename=${filePath}&value=${encoded}`; + url += `/${repoInfo.owner}/${repoInfo.repo}/new/${branch}?filename=${filePath}&value=${encoded}`; window.navigateTo(url); @@ -50,10 +54,24 @@ function showDownloadAlert() { alert.focus(); } -function handleDownloadClick(e) { +async function handleDownloadClick(e) { e.preventDefault(); - const workflow = generateWorkflowWithFormInputs(); + const repoField = document.getElementById("repo"); + const repoInfo = parseRepositoryURL(repoField.value.trim()); + + let branch = await detectDefaultBranch(repoInfo); + if (!branch) branch = "main"; + + const workflow = generateWorkflow({ + useBatchToken: document.getElementById("use-batch-token").checked, + useVirtualDisplay: document.getElementById("use-virtual-display").checked, + buildAcrossPlatforms: document.getElementById("build-across-platforms") + .checked, + siteUrl: + window.location.origin + window.location.pathname.replace(/\/[^/]*$/, ""), + branch, + }); const blob = new Blob([workflow], { type: "text/yaml" }); const url = URL.createObjectURL(blob); diff --git a/public/scripts/workflow.js b/public/scripts/workflow.js index b4560d2..97cd73c 100644 --- a/public/scripts/workflow.js +++ b/public/scripts/workflow.js @@ -33,6 +33,7 @@ function parseRepositoryURL(repoURL) { // Enterprise: http(s)://github.com/enterprises/enterprise/owner/repo return { origin: url.origin, + hostname: url.hostname, enterprise: parts[1], owner: parts[2], repo: parts[3], @@ -41,6 +42,7 @@ function parseRepositoryURL(repoURL) { // Standard: http(s)://host/owner/repo return { origin: url.origin, + hostname: url.hostname, owner: parts[0], repo: parts[1], }; @@ -51,11 +53,88 @@ function parseRepositoryURL(repoURL) { return null; } +async function detectDefaultBranch(repoInfo) { + if (!repoInfo) return null; + + async function fetchWithTimeout(url, options = {}) { + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), 3000); // 3 seconds timeout + try { + const resp = await fetch(url, { + ...options, + signal: controller.signal, + }); + return resp; + } finally { + clearTimeout(timeout); + } + } + + // Try to detect default branch using the GitHub API + async function tryApi() { + let apiUrl; + if (repoInfo.hostname.replace(/^www\./, "") === "github.com") { + apiUrl = `https://api.github.com/repos/${repoInfo.owner}/${repoInfo.repo}`; + } else { + apiUrl = `${repoInfo.origin}/api/v3/repos/${repoInfo.owner}/${repoInfo.repo}`; + } + + try { + const resp = await fetchWithTimeout(apiUrl, { + headers: { Accept: "application/vnd.github+json" }, + }); + if (!resp.ok) return null; + + const data = await resp.json(); + return data.default_branch || null; + } catch { + // Network or abort error, fallback + return null; + } + } + + // Fallback: Probe common branch names by checking for a README.md + async function tryBlobFallback() { + let repoBaseUrl; + if (repoInfo.hostname.replace(/^www\./, "") === "github.com") { + repoBaseUrl = "https://github.com"; + } else { + repoBaseUrl = `${repoInfo.origin}`; + } + if (repoInfo.enterprise) { + repoBaseUrl += `/enterprises/${repoInfo.enterprise}`; + } + repoBaseUrl += `/${repoInfo.owner}/${repoInfo.repo}`; + + const branches = ["main", "master", "develop"]; + const checks = branches.map(async (branch) => { + const url = `${repoBaseUrl}/blob/${branch}/README.md`; + try { + const resp = await fetchWithTimeout(url, { + method: "HEAD", + redirect: "manual", + }); + return resp.ok ? branch : null; + } catch { + return null; + } + }); + const results = await Promise.all(checks); + return results.find((branch) => branch !== null) || null; + } + + // Main logic: Try API first, then fallback if needed + const apiBranch = await tryApi(); + if (apiBranch) return apiBranch; + return await tryBlobFallback(); +} + function generateWorkflow({ useBatchToken = false, useVirtualDisplay = false, buildAcrossPlatforms = false, siteUrl = "http://localhost/", + branch = "main", }) { return dedent(` # This workflow was generated using the GitHub Actions Workflow Generator for MATLAB. @@ -65,9 +144,9 @@ function generateWorkflow({ on: push: - branches: [main] + branches: [${branch}] pull_request: - branches: [main] + branches: [${branch}] workflow_dispatch: {} ${ useBatchToken @@ -145,4 +224,4 @@ function dedent(str) { return match ? str.replace(new RegExp("^" + match[0], "gm"), "") : str; } -export { parseRepositoryURL, generateWorkflow }; +export { parseRepositoryURL, detectDefaultBranch, generateWorkflow }; diff --git a/tests/main.test.js b/tests/main.test.js index b7a838f..00ac194 100644 --- a/tests/main.test.js +++ b/tests/main.test.js @@ -20,38 +20,49 @@ beforeEach(async () => {
`; + const realWorkflow = await import("../public/scripts/workflow.js"); + jest.unstable_mockModule("../public/scripts/workflow.js", () => ({ + ...realWorkflow, + detectDefaultBranch: jest.fn().mockResolvedValue("main"), + })); + await import("../public/scripts/main.js"); window.navigateTo = jest.fn(); }); -test("form submit with invalid repo shows error", () => { +const flushPromises = () => new Promise((r) => setTimeout(r, 0)); + +test("form submit with invalid repo shows error", async () => { const repoInput = document.getElementById("repo"); expect(repoInput.classList.contains("is-invalid")).toBe(false); repoInput.value = "invalidrepo"; document .getElementById("generate-form") .dispatchEvent(new Event("submit", { bubbles: true, cancelable: true })); + await flushPromises(); expect(repoInput.classList.contains("is-invalid")).toBe(true); expect(window.navigateTo).not.toHaveBeenCalled(); }); -test("form submit with valid slug works", () => { +test("form submit with valid slug works", async () => { const repoInput = document.getElementById("repo"); repoInput.value = "owner/repo"; document .getElementById("generate-form") .dispatchEvent(new Event("submit", { bubbles: true, cancelable: true })); + await flushPromises(); expect(window.navigateTo).toHaveBeenCalledWith( expect.stringContaining("https://github.com/owner/repo/new/main?filename="), ); }); -test("form submit with valid URL works", () => { +test("form submit with valid URL works", async () => { const repoInput = document.getElementById("repo"); repoInput.value = "https://github.com/octocat/Hello-World"; document .getElementById("generate-form") .dispatchEvent(new Event("submit", { bubbles: true, cancelable: true })); + await flushPromises(); expect(window.navigateTo).toHaveBeenCalledWith( expect.stringContaining( "https://github.com/octocat/Hello-World/new/main?filename=", @@ -59,12 +70,13 @@ test("form submit with valid URL works", () => { ); }); -test("form submit with valid cloud-hosted enterprise URL works", () => { +test("form submit with valid cloud-hosted enterprise URL works", async () => { const repoInput = document.getElementById("repo"); repoInput.value = "https://github.com/enterprises/gh/octocat/Hello-World"; document .getElementById("generate-form") .dispatchEvent(new Event("submit", { bubbles: true, cancelable: true })); + await flushPromises(); expect(window.navigateTo).toHaveBeenCalledWith( expect.stringContaining( "https://github.com/enterprises/gh/octocat/Hello-World/new/main?filename=", @@ -72,12 +84,57 @@ test("form submit with valid cloud-hosted enterprise URL works", () => { ); }); +test("form submit uses detected default branch", async () => { + jest.resetModules(); + jest.unstable_mockModule("../public/scripts/workflow.js", () => ({ + parseRepositoryURL: (v) => ({ + origin: "https://github.com", + owner: "o", + repo: "r", + }), + detectDefaultBranch: jest.fn().mockResolvedValue("master"), + generateWorkflow: () => "yaml-content", + })); + await import("../public/scripts/main.js"); + window.navigateTo = jest.fn(); + document + .getElementById("generate-form") + .dispatchEvent(new Event("submit", { bubbles: true, cancelable: true })); + await flushPromises(); + expect(window.navigateTo).toHaveBeenCalledWith( + expect.stringContaining("https://github.com/o/r/new/master?filename="), + ); +}); + +test("form submit defaults to main branch if detection fails", async () => { + jest.resetModules(); + jest.unstable_mockModule("../public/scripts/workflow.js", () => ({ + parseRepositoryURL: (v) => ({ + origin: "https://github.com", + owner: "o", + repo: "r", + }), + detectDefaultBranch: jest.fn().mockResolvedValue(null), + generateWorkflow: () => "yaml-content", + })); + await import("../public/scripts/main.js"); + window.navigateTo = jest.fn(); + document + .getElementById("generate-form") + .dispatchEvent(new Event("submit", { bubbles: true, cancelable: true })); + await flushPromises(); + expect(window.navigateTo).toHaveBeenCalledWith( + expect.stringContaining("https://github.com/o/r/new/main?filename="), + ); +}); + test("advanced options are passed to generateWorkflow", async () => { // Re-import main.js with a spy on generateWorkflow jest.resetModules(); const workflowSpy = jest.fn(() => "yaml-content"); jest.unstable_mockModule("../public/scripts/workflow.js", () => ({ parseRepositoryURL: (v) => ({ owner: "o", repo: "r" }), + detectDefaultBranch: jest.fn().mockResolvedValue("main"), generateWorkflow: workflowSpy, })); document.getElementById("repo").value = "o/r"; @@ -89,15 +146,17 @@ test("advanced options are passed to generateWorkflow", async () => { document .getElementById("generate-form") .dispatchEvent(new Event("submit", { bubbles: true, cancelable: true })); + await flushPromises(); expect(workflowSpy).toHaveBeenCalledWith({ useBatchToken: true, useVirtualDisplay: false, buildAcrossPlatforms: true, siteUrl: "http://localhost", + branch: "main", }); }); -test("download link triggers file download", () => { +test("download link triggers file download", async () => { const repoInput = document.getElementById("repo"); repoInput.value = "owner/repo"; @@ -116,6 +175,8 @@ test("download link triggers file download", () => { document.getElementById("download-alert-link").click(); + await flushPromises(); + expect(mockCreateObjectURL).toHaveBeenCalled(); expect(clickSpy).toHaveBeenCalled(); expect(mockRevokeObjectURL).toHaveBeenCalled(); diff --git a/tests/workflow.test.js b/tests/workflow.test.js index a0b4087..ed31ebb 100644 --- a/tests/workflow.test.js +++ b/tests/workflow.test.js @@ -1,3 +1,4 @@ +import { jest } from "@jest/globals"; import { parseRepositoryURL, generateWorkflow, @@ -9,6 +10,7 @@ describe("parseRepositoryURL", () => { test("shorthand owner/repo", () => { expect(parseRepositoryURL("owner/repo")).toEqual({ origin: "https://github.com", + hostname: "github.com", owner: "owner", repo: "repo", }); @@ -18,6 +20,7 @@ describe("parseRepositoryURL", () => { parseRepositoryURL("https://github.com/octocat/hello-world"), ).toEqual({ origin: "https://github.com", + hostname: "github.com", owner: "octocat", repo: "hello-world", }); @@ -25,6 +28,7 @@ describe("parseRepositoryURL", () => { parseRepositoryURL("http://github.com/octocat/hello-world"), ).toEqual({ origin: "http://github.com", + hostname: "github.com", owner: "octocat", repo: "hello-world", }); @@ -36,6 +40,7 @@ describe("parseRepositoryURL", () => { ), ).toEqual({ origin: "https://github.com", + hostname: "github.com", enterprise: "mycompany", owner: "octocat", repo: "hello-world", @@ -44,6 +49,7 @@ describe("parseRepositoryURL", () => { parseRepositoryURL("https://mycompany.github.com/octocat/hello-world"), ).toEqual({ origin: "https://mycompany.github.com", + hostname: "mycompany.github.com", owner: "octocat", repo: "hello-world", }); @@ -51,6 +57,7 @@ describe("parseRepositoryURL", () => { parseRepositoryURL("https://github.mycompany.com/octocat/hello-world"), ).toEqual({ origin: "https://github.mycompany.com", + hostname: "github.mycompany.com", owner: "octocat", repo: "hello-world", }); @@ -58,6 +65,7 @@ describe("parseRepositoryURL", () => { test("URLs without protocol", () => { expect(parseRepositoryURL("github.com/octocat/hello-world")).toEqual({ origin: "https://github.com", + hostname: "github.com", owner: "octocat", repo: "hello-world", }); @@ -65,6 +73,7 @@ describe("parseRepositoryURL", () => { parseRepositoryURL("mycompany.github.com/octocat/hello-world"), ).toEqual({ origin: "https://mycompany.github.com", + hostname: "mycompany.github.com", owner: "octocat", repo: "hello-world", }); @@ -72,6 +81,7 @@ describe("parseRepositoryURL", () => { parseRepositoryURL("github.mycompany.com/octocat/hello-world"), ).toEqual({ origin: "https://github.mycompany.com", + hostname: "github.mycompany.com", owner: "octocat", repo: "hello-world", }); @@ -81,6 +91,7 @@ describe("parseRepositoryURL", () => { parseRepositoryURL("https://github.com/octocat/hello-world/"), ).toEqual({ origin: "https://github.com", + hostname: "github.com", owner: "octocat", repo: "hello-world", }); @@ -88,6 +99,7 @@ describe("parseRepositoryURL", () => { parseRepositoryURL("https://github.com/octocat/hello-world/README.md"), ).toEqual({ origin: "https://github.com", + hostname: "github.com", owner: "octocat", repo: "hello-world", }); @@ -95,6 +107,7 @@ describe("parseRepositoryURL", () => { parseRepositoryURL("https://github.com/octocat/hello-world.git"), ).toEqual({ origin: "https://github.com", + hostname: "github.com", owner: "octocat", repo: "hello-world", }); @@ -107,6 +120,7 @@ describe("parseRepositoryURL", () => { parseRepositoryURL("git@github.com:octocat/hello-world.git"), ).toEqual({ origin: "https://github.com", + hostname: "github.com", owner: "octocat", repo: "hello-world", }); @@ -114,6 +128,7 @@ describe("parseRepositoryURL", () => { parseRepositoryURL("ssh://git@github.com/octocat/hello-world.git"), ).toEqual({ origin: "https://github.com", + hostname: "github.com", owner: "octocat", repo: "hello-world", }); @@ -123,6 +138,7 @@ describe("parseRepositoryURL", () => { ), ).toEqual({ origin: "https://github.com", + hostname: "github.com", enterprise: "mycompany", owner: "octocat", repo: "hello-world", @@ -133,6 +149,7 @@ describe("parseRepositoryURL", () => { ), ).toEqual({ origin: "https://mycompany.github.com", + hostname: "mycompany.github.com", owner: "octocat", repo: "hello-world", }); @@ -140,6 +157,7 @@ describe("parseRepositoryURL", () => { parseRepositoryURL("git@github.mycompany.com:octocat/hello-world.git"), ).toEqual({ origin: "https://github.mycompany.com", + hostname: "github.mycompany.com", owner: "octocat", repo: "hello-world", }); @@ -149,6 +167,7 @@ describe("parseRepositoryURL", () => { ), ).toEqual({ origin: "https://github.mycompany.com", + hostname: "github.mycompany.com", owner: "octocat", repo: "hello-world", }); @@ -156,6 +175,7 @@ describe("parseRepositoryURL", () => { test("SSH URLs without .git", () => { expect(parseRepositoryURL("git@github.com:octocat/hello-world")).toEqual({ origin: "https://github.com", + hostname: "github.com", owner: "octocat", repo: "hello-world", }); @@ -165,6 +185,7 @@ describe("parseRepositoryURL", () => { parseRepositoryURL("git://github.com/octocat/hello-world.git"), ).toEqual({ origin: "https://github.com", + hostname: "github.com", owner: "octocat", repo: "hello-world", }); @@ -172,6 +193,7 @@ describe("parseRepositoryURL", () => { parseRepositoryURL("git://github.com/octocat/hello-world"), ).toEqual({ origin: "https://github.com", + hostname: "github.com", owner: "octocat", repo: "hello-world", }); @@ -182,11 +204,13 @@ describe("parseRepositoryURL", () => { test("protocol/host case insensitivity, preserve owner/repo case", () => { expect(parseRepositoryURL("OWNER/REPO")).toEqual({ origin: "https://github.com", + hostname: "github.com", owner: "OWNER", repo: "REPO", }); expect(parseRepositoryURL("GitHub.com/OctoCat/Hello-World")).toEqual({ origin: "https://github.com", + hostname: "github.com", owner: "OctoCat", repo: "Hello-World", }); @@ -194,6 +218,7 @@ describe("parseRepositoryURL", () => { parseRepositoryURL("HTTPS://GITHUB.COM/OctoCat/Hello-World"), ).toEqual({ origin: "https://github.com", + hostname: "github.com", owner: "OctoCat", repo: "Hello-World", }); @@ -201,6 +226,7 @@ describe("parseRepositoryURL", () => { parseRepositoryURL("git@github.com:OctoCat/Hello-World.git"), ).toEqual({ origin: "https://github.com", + hostname: "github.com", owner: "OctoCat", repo: "Hello-World", }); @@ -208,6 +234,7 @@ describe("parseRepositoryURL", () => { parseRepositoryURL("ssh://git@github.com/OctoCat/Hello-World.git"), ).toEqual({ origin: "https://github.com", + hostname: "github.com", owner: "OctoCat", repo: "Hello-World", }); @@ -215,6 +242,7 @@ describe("parseRepositoryURL", () => { parseRepositoryURL("git://github.com/OctoCat/Hello-World.git"), ).toEqual({ origin: "https://github.com", + hostname: "github.com", owner: "OctoCat", repo: "Hello-World", }); @@ -240,6 +268,7 @@ describe("parseRepositoryURL", () => { parseRepositoryURL("https://github.com:8080/octocat/hello-world"), ).toEqual({ origin: "https://github.com:8080", + hostname: "github.com", owner: "octocat", repo: "hello-world", }); @@ -249,6 +278,7 @@ describe("parseRepositoryURL", () => { ), ).toEqual({ origin: "https://github.mycompany.com:8080", + hostname: "github.mycompany.com", owner: "octocat", repo: "hello-world", }); @@ -256,6 +286,7 @@ describe("parseRepositoryURL", () => { test("inputs with whitespace", () => { expect(parseRepositoryURL(" owner/repo ")).toEqual({ origin: "https://github.com", + hostname: "github.com", owner: "owner", repo: "repo", }); @@ -263,6 +294,7 @@ describe("parseRepositoryURL", () => { parseRepositoryURL(" https://github.com/octocat/hello-world "), ).toEqual({ origin: "https://github.com", + hostname: "github.com", owner: "octocat", repo: "hello-world", }); @@ -270,6 +302,163 @@ describe("parseRepositoryURL", () => { }); }); +describe("detectDefaultBranch", () => { + // Mock fetch globally for all tests in this suite + const originalFetch = global.fetch; + afterEach(() => { + global.fetch = originalFetch; + jest.clearAllMocks(); + }); + + test("returns null for null input", async () => { + const { detectDefaultBranch } = await import( + "../public/scripts/workflow.js" + ); + const branch = await detectDefaultBranch(null); + expect(branch).toBeNull(); + }); + + test("detects default branch via GitHub API", async () => { + global.fetch = jest.fn().mockResolvedValue({ + ok: true, + json: () => Promise.resolve({ default_branch: "dev" }), + }); + const { detectDefaultBranch } = await import( + "../public/scripts/workflow.js" + ); + const repoInfo = { + origin: "https://github.com", + hostname: "github.com", + owner: "o", + repo: "r", + }; + const branch = await detectDefaultBranch(repoInfo); + expect(branch).toBe("dev"); + expect(global.fetch).toHaveBeenCalledWith( + "https://api.github.com/repos/o/r", + expect.any(Object), + ); + }); + + test("detects default branch via GitHub Enterprise API", async () => { + global.fetch = jest.fn().mockResolvedValue({ + ok: true, + json: () => Promise.resolve({ default_branch: "dev" }), + }); + const { detectDefaultBranch } = await import( + "../public/scripts/workflow.js" + ); + const repoInfo = { + origin: "https://mycompany.github.com", + hostname: "mycompany.github.com", + owner: "o", + repo: "r", + }; + const branch = await detectDefaultBranch(repoInfo); + expect(branch).toBe("dev"); + expect(global.fetch).toHaveBeenCalledWith( + "https://mycompany.github.com/api/v3/repos/o/r", + expect.any(Object), + ); + }); + + test("returns null if GitHub API response is not ok", async () => { + global.fetch = jest.fn().mockResolvedValue({ ok: false }); + const { detectDefaultBranch } = await import( + "../public/scripts/workflow.js" + ); + const repoInfo = { + origin: "https://github.com", + hostname: "github.com", + owner: "o", + repo: "r", + }; + const branch = await detectDefaultBranch(repoInfo); + expect(branch).toBeNull(); + }); + + test("detects 'main' branch on fallback", async () => { + global.fetch = jest.fn().mockImplementation((url) => { + return Promise.resolve({ ok: url.includes("main") }); + }); + const { detectDefaultBranch } = await import( + "../public/scripts/workflow.js" + ); + const repoInfo = { + origin: "https://github.com", + hostname: "github.com", + owner: "o", + repo: "r", + }; + const branch = await detectDefaultBranch(repoInfo); + expect(branch).toBe("main"); + }); + + test("detects 'master' branch on fallback", async () => { + global.fetch = jest.fn().mockImplementation((url) => { + return Promise.resolve({ ok: url.includes("master") }); + }); + const { detectDefaultBranch } = await import( + "../public/scripts/workflow.js" + ); + const repoInfo = { + origin: "https://github.com", + hostname: "github.com", + owner: "o", + repo: "r", + }; + const branch = await detectDefaultBranch(repoInfo); + expect(branch).toBe("master"); + }); + + test("detects 'develop' branch on fallback", async () => { + global.fetch = jest.fn().mockImplementation((url) => { + return Promise.resolve({ ok: url.includes("develop") }); + }); + const { detectDefaultBranch } = await import( + "../public/scripts/workflow.js" + ); + const repoInfo = { + origin: "https://github.com", + hostname: "github.com", + owner: "o", + repo: "r", + }; + const branch = await detectDefaultBranch(repoInfo); + expect(branch).toBe("develop"); + }); + + test("returns null if no fallback branches found", async () => { + global.fetch = jest.fn().mockResolvedValue({ ok: false }); + const { detectDefaultBranch } = await import( + "../public/scripts/workflow.js" + ); + const repoInfo = { + origin: "https://github.com", + hostname: "github.com", + owner: "o", + repo: "r", + }; + const branch = await detectDefaultBranch(repoInfo); + expect(branch).toBeNull(); + }); + + test("handles fetch errors gracefully", async () => { + global.fetch = jest.fn().mockRejectedValue(new Error("Network error")); + const { detectDefaultBranch } = await import( + "../public/scripts/workflow.js" + ); + const repoInfo = { + origin: "https://github.com", + hostname: "github.com", + owner: "o", + repo: "r", + }; + const branch = await detectDefaultBranch(repoInfo); + expect(branch).toBeNull(); + }); +}); + describe("generateWorkflow", () => { test("default workflow", () => { const yaml = generateWorkflow({});