diff --git a/.changeset/tame-hotels-attack.md b/.changeset/tame-hotels-attack.md new file mode 100644 index 00000000000..b05f4e98efa --- /dev/null +++ b/.changeset/tame-hotels-attack.md @@ -0,0 +1,6 @@ +--- +"remix": patch +"@remix-run/dev": patch +--- + +Allow private GitHub repos to use the latest release url for tarballs when using `create-remix` diff --git a/contributors.yml b/contributors.yml index a5e5e428a08..b5da0d2dadf 100644 --- a/contributors.yml +++ b/contributors.yml @@ -196,6 +196,7 @@ - jodygeraldo - joelazar - johannesbraeunig +- johnmberger - johnpolacek - johnson444 - joms diff --git a/docs/other-api/dev.md b/docs/other-api/dev.md index 5abcfd5b35f..66c58da4d79 100644 --- a/docs/other-api/dev.md +++ b/docs/other-api/dev.md @@ -40,6 +40,7 @@ remix create ./my-app --template :username/:repo remix create ./my-app --template https://github.com/:username/:repo remix create ./my-app --template https://github.com/:username/:repo/tree/:branch remix create ./my-app --template https://github.com/:username/:repo/archive/refs/tags/:tag.tar.gz +remix create ./my-app --template https://github.com/:username/:repo/releases/latest/download/:tag.tar.gz remix create ./my-app --template https://example.com/remix-template.tar.gz ``` diff --git a/packages/remix-dev/__tests__/create-test.ts b/packages/remix-dev/__tests__/create-test.ts index 0956d39c240..b164b874a72 100644 --- a/packages/remix-dev/__tests__/create-test.ts +++ b/packages/remix-dev/__tests__/create-test.ts @@ -266,7 +266,7 @@ describe("the create command", () => { "create", projectDir, "--template", - "https://example.com/remix-stack.tar.gz", + "https://github.com/private-org/private-repo/releases/download/v0.0.1/stack.tar.gz", "--no-install", "--typescript", "--token", diff --git a/packages/remix-dev/cli/create.ts b/packages/remix-dev/cli/create.ts index f3dc8ebd285..40feced27eb 100644 --- a/packages/remix-dev/cli/create.ts +++ b/packages/remix-dev/cli/create.ts @@ -307,10 +307,17 @@ async function downloadAndExtractTarball( // asset id let info = getGithubReleaseAssetInfo(url); headers.Accept = "application/vnd.github.v3+json"; - let response = await fetch( - `https://api.github.com/repos/${info.owner}/${info.name}/releases/tags/${info.tag}`, - { agent: agent("https://api.github.com"), headers } - ); + + let releaseUrl = + info.tag === "latest" + ? `https://api.github.com/repos/${info.owner}/${info.name}/releases/latest` + : `https://api.github.com/repos/${info.owner}/${info.name}/releases/tags/${info.tag}`; + + let response = await fetch(releaseUrl, { + agent: agent("https://api.github.com"), + headers, + }); + if (response.status !== 200) { throw Error( "🚨 There was a problem fetching the file from GitHub. The request " + @@ -318,9 +325,13 @@ async function downloadAndExtractTarball( ); } let body = await response.json(); - let assetId: number | undefined = body?.assets?.find( - (a: any) => a?.browser_download_url === url - )?.id; + // If the release is "latest", the url won't match the download url, so we grab the id from the response + let assetId: number | undefined = + info.tag === "latest" + ? body?.assets?.find((a: any) => + a?.browser_download_url?.includes(info.asset) + )?.id + : body?.assets?.find((a: any) => a?.browser_download_url === url)?.id; if (!assetId) { throw Error( "🚨 There was a problem fetching the file from GitHub. No asset was " + @@ -434,8 +445,16 @@ function getGithubUrl(info: Omit) { } function isGithubReleaseAssetUrl(url: string) { + /** + * Accounts for the following formats: + * https://github.com/owner/repository/releases/download/v0.0.1/stack.tar.gz + * ~or~ + * https://github.com/owner/repository/releases/latest/download/stack.tar.gz + */ return ( - url.startsWith("https://github.com") && url.includes("/releases/download/") + url.startsWith("https://github.com") && + (url.includes("/releases/download/") || + url.includes("/releases/latest/download/")) ); } interface ReleaseAssetInfo { @@ -446,18 +465,30 @@ interface ReleaseAssetInfo { tag: string; } function getGithubReleaseAssetInfo(browserUrl: string): ReleaseAssetInfo { - // for example, https://github.com/owner/repository/releases/download/v0.0.1/stack.tar.gz + /** + * https://github.com/owner/repository/releases/download/v0.0.1/stack.tar.gz + * ~or~ + * https://github.com/owner/repository/releases/latest/download/stack.tar.gz + */ + let url = new URL(browserUrl); - let [, owner, name, , , tag, asset] = url.pathname.split("/") as [ + let [, owner, name, , downloadOrLatest, tag, asset] = url.pathname.split( + "/" + ) as [ _: string, Owner: string, Name: string, Releases: string, - Download: string, + DownloadOrLatest: string, Tag: string, AssetFilename: string ]; + if (downloadOrLatest === "latest" && tag === "download") { + // handle the Github URL quirk for latest releases + tag = "latest"; + } + return { browserUrl, owner, @@ -583,7 +614,10 @@ export async function validateTemplate( let headers: Record = {}; if (isGithubReleaseAssetUrl(input)) { let info = getGithubReleaseAssetInfo(input); - apiUrl = `https://api.github.com/repos/${info.owner}/${info.name}/releases/tags/${info.tag}`; + apiUrl = + info.tag === "latest" + ? `https://api.github.com/repos/${info.owner}/${info.name}/releases/latest` + : `https://api.github.com/repos/${info.owner}/${info.name}/releases/tags/${info.tag}`; headers = { Authorization: `token ${options?.githubToken}`, Accept: "application/vnd.github.v3+json", @@ -609,9 +643,17 @@ export async function validateTemplate( switch (response.status) { case 200: if (isGithubReleaseAssetUrl(input)) { + let info = getGithubReleaseAssetInfo(input); let body = await response.json(); if ( - !body?.assets?.some((a: any) => a?.browser_download_url === input) + // if a tag is specified, make sure it exists. + !body?.assets?.some( + (a: any) => a?.browser_download_url === input + ) && + // if the latest is specified, make sure there is an asset + !body?.assets?.some((a: any) => + a?.browser_download_url.includes(info.asset) + ) ) { throw Error( "🚨 The template file could not be verified. Please double check " +