diff --git a/lib/definitions/errors.js b/lib/definitions/errors.js index 9dc7eca8..74fa1ce2 100644 --- a/lib/definitions/errors.js +++ b/lib/definitions/errors.js @@ -139,6 +139,19 @@ By default the \`repositoryUrl\` option is retrieved from the \`repository\` pro }; } +export function EMISMATCHGITHUBURL({ repositoryUrl, clone_url }) { + return { + message: "The git repository URL mismatches the GitHub URL.", + details: `The **semantic-release** \`repositoryUrl\` option must have the same repository name and owner as the GitHub repo. + +Your configuration for the \`repositoryUrl\` option is \`${stringify(repositoryUrl)}\` and the \`clone_url\` of your GitHub repo is \`${stringify(clone_url)}\`. + +By default the \`repositoryUrl\` option is retrieved from the \`repository\` property of your \`package.json\` or the [git origin url](https://git-scm.com/book/en/v2/Git-Basics-Working-with-Remotes) of the repository cloned by your CI environment. + +Note: If you have recently changed your GitHub repository name or owner, update the value in **semantic-release** \`repositoryUrl\` option and the \`repository\` property of your \`package.json\` respectively to match the new GitHub URL.`, + }; +} + export function EINVALIDPROXY({ proxy }) { return { message: "Invalid `proxy` option.", diff --git a/lib/verify.js b/lib/verify.js index b951cee3..b6ccba95 100644 --- a/lib/verify.js +++ b/lib/verify.js @@ -103,35 +103,34 @@ export default async function verify(pluginConfig, context, { Octokit }) { proxy, }), ); - - // https://github.com/semantic-release/github/issues/182 - // Do not check for permissions in GitHub actions, as the provided token is an installation access token. - // octokit.request("GET /repos/{owner}/{repo}", {repo, owner}) does not return the "permissions" key in that case. - // But GitHub Actions have all permissions required for @semantic-release/github to work - if (env.GITHUB_ACTION) { - return; - } - try { const { - data: { - permissions: { push }, - }, + data: { permissions, clone_url }, } = await octokit.request("GET /repos/{owner}/{repo}", { repo, owner }); - if (!push) { + // Verify if Repository Name wasn't changed + const parsedCloneUrl = parseGithubUrl(clone_url); + if (owner !== parsedCloneUrl.owner || repo !== parsedCloneUrl.repo) { + errors.push( + getError("EMISMATCHGITHUBURL", { repositoryUrl, clone_url }), + ); + } + + // https://github.com/semantic-release/github/issues/182 + // Do not check for permissions in GitHub actions, as the provided token is an installation access token. + // octokit.request("GET /repos/{owner}/{repo}", {repo, owner}) does not return the "permissions" key in that case. + // But GitHub Actions have all permissions required for @semantic-release/github to work + if (!env.GITHUB_ACTION && !permissions?.push) { // If authenticated as GitHub App installation, `push` will always be false. // We send another request to check if current authentication is an installation. // Note: we cannot check if the installation has all required permissions, it's // up to the user to make sure it has if ( - await octokit + !(await octokit .request("HEAD /installation/repositories", { per_page: 1 }) - .catch(() => false) + .catch(() => false)) ) { - return; + errors.push(getError("EGHNOPERMISSION", { owner, repo })); } - - errors.push(getError("EGHNOPERMISSION", { owner, repo })); } } catch (error) { if (error.status === 401) { diff --git a/test/fail.test.js b/test/fail.test.js index deb8b79d..9b7caee9 100644 --- a/test/fail.test.js +++ b/test/fail.test.js @@ -115,6 +115,7 @@ test("Open a new issue with the list of errors and custom title and comment", as .sandbox() .getOnce(`https://api.github.local/repos/${owner}/${repo}`, { full_name: `${owner}/${repo}`, + clone_url: `https://api.github.local/${owner}/${repo}.git`, }) .getOnce( `https://api.github.local/search/issues?q=${encodeURIComponent( @@ -182,6 +183,7 @@ test("Open a new issue with assignees and the list of errors", async (t) => { .sandbox() .getOnce(`https://api.github.local/repos/${owner}/${repo}`, { full_name: `${owner}/${repo}`, + clone_url: `https://api.github.local/${owner}/${repo}.git`, }) .getOnce( `https://api.github.local/search/issues?q=${encodeURIComponent( @@ -254,6 +256,7 @@ test("Open a new issue without labels and the list of errors", async (t) => { .sandbox() .getOnce(`https://api.github.local/repos/${owner}/${repo}`, { full_name: `${owner}/${repo}`, + clone_url: `https://api.github.local/${owner}/${repo}.git`, }) .getOnce( `https://api.github.local/search/issues?q=${encodeURIComponent( @@ -330,6 +333,7 @@ test("Update the first existing issue with the list of errors", async (t) => { .sandbox() .getOnce(`https://api.github.local/repos/${owner}/${repo}`, { full_name: `${owner}/${repo}`, + clone_url: `https://api.github.local/${owner}/${repo}.git`, }) .getOnce( `https://api.github.local/search/issues?q=${encodeURIComponent( diff --git a/test/integration.test.js b/test/integration.test.js index b9f2f299..030a7a54 100644 --- a/test/integration.test.js +++ b/test/integration.test.js @@ -27,7 +27,10 @@ test("Verify GitHub auth", async (t) => { const fetch = fetchMock .sandbox() .getOnce(`https://api.github.local/repos/${owner}/${repo}`, { - permissions: { push: true }, + permissions: { + push: true, + }, + clone_url: `https://api.github.local/${owner}/${repo}.git`, }); await t.notThrowsAsync( @@ -56,8 +59,11 @@ test("Verify GitHub auth with publish options", async (t) => { }; const fetch = fetchMock .sandbox() - .getOnce(`https://api.github.local/repos/${owner}/${repo}`, { - permissions: { push: true }, + .get(`https://api.github.local/repos/${owner}/${repo}`, { + permissions: { + push: true, + }, + clone_url: `https://api.github.local/${owner}/${repo}.git`, }); await t.notThrowsAsync( @@ -94,7 +100,10 @@ test("Verify GitHub auth and assets config", async (t) => { const fetch = fetchMock .sandbox() .getOnce(`https://api.github.local/repos/${owner}/${repo}`, { - permissions: { push: true }, + permissions: { + push: true, + }, + clone_url: `https://api.github.local/${owner}/${repo}.git`, }); await t.notThrowsAsync( @@ -197,7 +206,10 @@ test("Publish a release with an array of assets", async (t) => { const fetch = fetchMock .sandbox() .getOnce(`https://api.github.local/repos/${owner}/${repo}`, { - permissions: { push: true }, + permissions: { + push: true, + }, + clone_url: `https://api.github.local/${owner}/${repo}.git`, }) .postOnce( `https://api.github.local/repos/${owner}/${repo}/releases`, @@ -289,7 +301,10 @@ test("Publish a release with release information in assets", async (t) => { const fetch = fetchMock .sandbox() .getOnce(`https://api.github.local/repos/${owner}/${repo}`, { - permissions: { push: true }, + permissions: { + push: true, + }, + clone_url: `https://api.github.local/${owner}/${repo}.git`, }) .postOnce( `https://api.github.local/repos/${owner}/${repo}/releases`, @@ -359,7 +374,10 @@ test("Update a release", async (t) => { const fetch = fetchMock .sandbox() .getOnce(`https://api.github.local/repos/${owner}/${repo}`, { - permissions: { push: true }, + permissions: { + push: true, + }, + clone_url: `https://api.github.local/${owner}/${repo}.git`, }) .getOnce( `https://api.github.local/repos/${owner}/${repo}/releases/tags/${nextRelease.gitTag}`, @@ -426,9 +444,9 @@ test("Comment and add labels on PR included in the releases", async (t) => { { permissions: { push: true }, full_name: `${owner}/${repo}`, + clone_url: `htttps://api.github.local/${owner}/${repo}.git`, }, { - // TODO: why do we call the same endpoint twice? repeat: 2, }, ) @@ -529,10 +547,9 @@ test("Open a new issue with the list of errors", async (t) => { { permissions: { push: true }, full_name: `${owner}/${repo}`, + clone_url: `htttps://api.github.local/${owner}/${repo}.git`, }, - { - repeat: 2, - }, + { repeat: 2 }, ) .getOnce( `https://api.github.local/search/issues?q=${encodeURIComponent( @@ -625,6 +642,7 @@ test("Verify, release and notify success", async (t) => { { permissions: { push: true }, full_name: `${owner}/${repo}`, + clone_url: `htttps://api.github.local/${owner}/${repo}.git`, }, { repeat: 2, @@ -785,6 +803,7 @@ test("Verify, update release and notify success", async (t) => { { permissions: { push: true }, full_name: `${owner}/${repo}`, + clone_url: `htttps://api.github.local/${owner}/${repo}.git`, }, { repeat: 2, @@ -917,6 +936,7 @@ test("Verify and notify failure", async (t) => { { permissions: { push: true }, full_name: `${owner}/${repo}`, + clone_url: `htttps://api.github.local/${owner}/${repo}.git`, }, { repeat: 2, diff --git a/test/success.test.js b/test/success.test.js index 8d87dccd..6bdc20b5 100644 --- a/test/success.test.js +++ b/test/success.test.js @@ -57,6 +57,7 @@ test("Add comment and labels to PRs associated with release commits and issues s .sandbox() .getOnce(`https://api.github.local/repos/${owner}/${repo}`, { full_name: `${redirectedOwner}/${redirectedRepo}`, + clone_url: `https://api.github.local/${owner}/${repo}.git`, }) .postOnce("https://api.github.local/graphql", { data: { @@ -418,6 +419,7 @@ test("Make multiple search queries if necessary", async (t) => { .sandbox() .getOnce(`https://api.github.local/repos/${owner}/${repo}`, { full_name: `${owner}/${repo}`, + clone_url: `https://api.github.local/${owner}/${repo}.git`, }) .post("https://api.github.local/graphql", { data: { @@ -662,6 +664,7 @@ test("Do not add comment and labels for unrelated PR returned by search (compare .sandbox() .getOnce(`https://api.github.local/repos/${owner}/${repo}`, { full_name: `${owner}/${repo}`, + clone_url: `https://api.github.local/${owner}/${repo}.git`, }) .postOnce("https://api.github.local/graphql", { data: { @@ -764,6 +767,7 @@ test("Do not add comment and labels if no PR is associated with release commits" .sandbox() .getOnce(`https://api.github.local/repos/${owner}/${repo}`, { full_name: `${owner}/${repo}`, + clone_url: `https://api.github.local/${owner}/${repo}.git`, }) .postOnce("https://api.github.local/graphql", { data: { @@ -826,6 +830,7 @@ test("Do not add comment and labels if no commits is found for release", async ( .sandbox() .getOnce(`https://api.github.local/repos/${owner}/${repo}`, { full_name: `${owner}/${repo}`, + clone_url: `https://api.github.local/${owner}/${repo}.git`, }) .getOnce( `https://api.github.local/search/issues?q=${encodeURIComponent( @@ -885,6 +890,7 @@ test("Do not add comment and labels to PR/issues from other repo", async (t) => .sandbox() .getOnce(`https://api.github.local/repos/${owner}/${repo}`, { full_name: `${owner}/${repo}`, + clone_url: `https://api.github.local/${owner}/${repo}.git`, }) .postOnce("https://api.github.local/graphql", { data: { @@ -985,6 +991,7 @@ test("Ignore missing and forbidden issues/PRs", async (t) => { .sandbox() .getOnce(`https://api.github.local/repos/${owner}/${repo}`, { full_name: `${owner}/${repo}`, + clone_url: `https://api.github.local/${owner}/${repo}.git`, }) .postOnce("https://api.github.local/graphql", { data: { @@ -1154,6 +1161,7 @@ test("Add custom comment and labels", async (t) => { .sandbox() .getOnce(`https://api.github.local/repos/${owner}/${repo}`, { full_name: `${owner}/${repo}`, + clone_url: `https://api.github.local/${owner}/${repo}.git`, }) .postOnce("https://api.github.local/graphql", { data: { @@ -1249,6 +1257,7 @@ test("Add custom label", async (t) => { .sandbox() .getOnce(`https://api.github.local/repos/${owner}/${repo}`, { full_name: `${owner}/${repo}`, + clone_url: `https://api.github.local/${owner}/${repo}.git`, }) .postOnce("https://api.github.local/graphql", { data: { @@ -1339,6 +1348,7 @@ test("Comment on issue/PR without ading a label", async (t) => { .sandbox() .getOnce(`https://api.github.local/repos/${owner}/${repo}`, { full_name: `${owner}/${repo}`, + clone_url: `https://api.github.local/${owner}/${repo}.git`, }) .postOnce("https://api.github.local/graphql", { data: { @@ -1432,6 +1442,7 @@ test("Editing the release to include all release links at the bottom", async (t) .sandbox() .getOnce(`https://api.github.local/repos/${owner}/${repo}`, { full_name: `${owner}/${repo}`, + clone_url: `https://api.github.local/${owner}/${repo}.git`, }) .postOnce("https://api.github.local/graphql", { data: { @@ -1536,6 +1547,7 @@ test("Editing the release to include all release links at the top", async (t) => .sandbox() .getOnce(`https://api.github.local/repos/${owner}/${repo}`, { full_name: `${owner}/${repo}`, + clone_url: `https://api.github.local/${owner}/${repo}.git`, }) .postOnce("https://api.github.local/graphql", { data: { @@ -1637,6 +1649,7 @@ test("Editing the release to include all release links with no additional releas .sandbox() .getOnce(`https://api.github.local/repos/${owner}/${repo}`, { full_name: `${owner}/${repo}`, + clone_url: `https://api.github.local/${owner}/${repo}.git`, }) .postOnce("https://api.github.local/graphql", { data: { @@ -1727,6 +1740,7 @@ test("Editing the release to include all release links with no additional releas .sandbox() .getOnce(`https://api.github.local/repos/${owner}/${repo}`, { full_name: `${owner}/${repo}`, + clone_url: `https://api.github.local/${owner}/${repo}.git`, }) .postOnce("https://api.github.local/graphql", { data: { @@ -1810,6 +1824,7 @@ test("Editing the release to include all release links with no releases", async .sandbox() .getOnce(`https://api.github.local/repos/${owner}/${repo}`, { full_name: `${owner}/${repo}`, + clone_url: `https://api.github.local/${owner}/${repo}.git`, }) .postOnce("https://api.github.local/graphql", { data: { @@ -1895,6 +1910,7 @@ test("Editing the release with no ID in the release", async (t) => { .sandbox() .getOnce(`https://api.github.local/repos/${owner}/${repo}`, { full_name: `${owner}/${repo}`, + clone_url: `https://api.github.local/${owner}/${repo}.git`, }) .postOnce("https://api.github.local/graphql", { data: { @@ -1985,6 +2001,7 @@ test("Ignore errors when adding comments and closing issues", async (t) => { .sandbox() .getOnce(`https://api.github.local/repos/${owner}/${repo}`, { full_name: `${owner}/${repo}`, + clone_url: `https://api.github.local/${owner}/${repo}.git`, }) .postOnce("https://api.github.local/graphql", { data: { @@ -2118,6 +2135,7 @@ test("Close open issues when a release is successful", async (t) => { .sandbox() .getOnce(`https://api.github.local/repos/${owner}/${repo}`, { full_name: `${owner}/${repo}`, + clone_url: `https://api.github.local/${owner}/${repo}.git`, }) .postOnce("https://api.github.local/graphql", { data: { @@ -2218,6 +2236,7 @@ test('Skip commention on issues/PR if "successComment" is "false"', async (t) => .sandbox() .getOnce(`https://api.github.local/repos/${owner}/${repo}`, { full_name: `${owner}/${repo}`, + clone_url: `https://api.github.local/${owner}/${repo}.git`, }) .getOnce( `https://api.github.local/search/issues?q=${encodeURIComponent( @@ -2269,6 +2288,7 @@ test('Skip closing issues if "failComment" is "false"', async (t) => { .sandbox() .getOnce(`https://api.github.local/repos/${owner}/${repo}`, { full_name: `${owner}/${repo}`, + clone_url: `https://api.github.local/${owner}/${repo}.git`, }) .postOnce("https://api.github.local/graphql", { data: { @@ -2320,6 +2340,7 @@ test('Skip closing issues if "failTitle" is "false"', async (t) => { .sandbox() .getOnce(`https://api.github.local/repos/${owner}/${repo}`, { full_name: `${owner}/${repo}`, + clone_url: `https://api.github.local/${owner}/${repo}.git`, }) .postOnce("https://api.github.local/graphql", { data: { diff --git a/test/verify.test.js b/test/verify.test.js index 9bac5d0b..37b3afb6 100644 --- a/test/verify.test.js +++ b/test/verify.test.js @@ -30,7 +30,10 @@ test("Verify package, token and repository access", async (t) => { const fetch = fetchMock .sandbox() .getOnce(`https://api.github.local/repos/${owner}/${repo}`, { - permissions: { push: true }, + permissions: { + push: true, + }, + clone_url: `https://api.github.local/${owner}/${repo}.git`, }); await t.notThrowsAsync( @@ -77,7 +80,10 @@ test('Verify package, token and repository access with "proxy", "asset", "discus const fetch = fetchMock .sandbox() .getOnce(`https://api.github.local/repos/${owner}/${repo}`, { - permissions: { push: true }, + permissions: { + push: true, + }, + clone_url: `https://api.github.local/${owner}/${repo}.git`, }); await t.notThrowsAsync( @@ -119,7 +125,10 @@ test("Verify package, token and repository access and custom URL with prefix", a const fetch = fetchMock .sandbox() .getOnce(`https://othertesturl.com:9090/prefix/repos/${owner}/${repo}`, { - permissions: { push: true }, + permissions: { + push: true, + }, + clone_url: `https://api.github.local/${owner}/${repo}.git`, }); await t.notThrowsAsync( @@ -157,7 +166,10 @@ test("Verify package, token and repository access and custom URL without prefix" const fetch = fetchMock .sandbox() .getOnce(`https://othertesturl.com:9090/repos/${owner}/${repo}`, { - permissions: { push: true }, + permissions: { + push: true, + }, + clone_url: `https://api.github.local/${owner}/${repo}.git`, }); await t.notThrowsAsync( @@ -195,7 +207,10 @@ test("Verify package, token and repository access and shorthand repositoryUrl UR const fetch = fetchMock .sandbox() .getOnce(`https://othertesturl.com:9090/repos/${owner}/${repo}`, { - permissions: { push: true }, + permissions: { + push: true, + }, + clone_url: `https://api.github.local/${owner}/${repo}.git`, }); await t.notThrowsAsync( @@ -234,7 +249,10 @@ test("Verify package, token and repository with environment variables", async (t const fetch = fetchMock .sandbox() .getOnce(`https://othertesturl.com:443/prefix/repos/${owner}/${repo}`, { - permissions: { push: true }, + permissions: { + push: true, + }, + clone_url: `htttps://api.github.local/${owner}/${repo}.git`, }); await t.notThrowsAsync( @@ -275,7 +293,10 @@ test("Verify package, token and repository access with alternative environment v const fetch = fetchMock .sandbox() .getOnce(`https://othertesturl.com:443/prefix/repos/${owner}/${repo}`, { - permissions: { push: true }, + permissions: { + push: true, + }, + clone_url: `htttps://api.github.local/${owner}/${repo}.git`, }); await t.notThrowsAsync( @@ -309,7 +330,10 @@ test("Verify package, token and repository access with custom API URL", async (t const fetch = fetchMock .sandbox() .getOnce(`https://api.othertesturl.com:9090/repos/${owner}/${repo}`, { - permissions: { push: true }, + permissions: { + push: true, + }, + clone_url: `htttps://api.github.local/${owner}/${repo}.git`, }); await t.notThrowsAsync( @@ -348,7 +372,10 @@ test("Verify package, token and repository access with API URL in environment va const fetch = fetchMock .sandbox() .getOnce(`https://api.othertesturl.com:443/repos/${owner}/${repo}`, { - permissions: { push: true }, + permissions: { + push: true, + }, + clone_url: `htttps://api.github.local/${owner}/${repo}.git`, }); await t.notThrowsAsync( @@ -381,7 +408,10 @@ test('Verify "proxy" is a String', async (t) => { const fetch = fetchMock .sandbox() .getOnce(`https://api.github.local/repos/${owner}/${repo}`, { - permissions: { push: true }, + permissions: { + push: true, + }, + clone_url: `https://api.github.local/${owner}/${repo}.git`, }); await t.notThrowsAsync( @@ -413,7 +443,10 @@ test('Verify "proxy" is an object with "host" and "port" properties', async (t) const fetch = fetchMock .sandbox() .getOnce(`https://api.github.local/repos/${owner}/${repo}`, { - permissions: { push: true }, + permissions: { + push: true, + }, + clone_url: `https://api.github.local/${owner}/${repo}.git`, }); await t.notThrowsAsync( @@ -447,7 +480,10 @@ test('Verify "proxy" is a Boolean set to false', async (t) => { const fetch = fetchMock .sandbox() .getOnce(`https://api.github.local/repos/${owner}/${repo}`, { - permissions: { push: true }, + permissions: { + push: true, + }, + clone_url: `https://api.github.local/${owner}/${repo}.git`, }); await t.notThrowsAsync( @@ -479,7 +515,10 @@ test('Verify "assets" is a String', async (t) => { const fetch = fetchMock .sandbox() .getOnce(`https://api.github.local/repos/${owner}/${repo}`, { - permissions: { push: true }, + permissions: { + push: true, + }, + clone_url: `https://api.github.local/${owner}/${repo}.git`, }); await t.notThrowsAsync( @@ -511,7 +550,10 @@ test('Verify "assets" is an Object with a path property', async (t) => { const fetch = fetchMock .sandbox() .getOnce(`https://api.github.local/repos/${owner}/${repo}`, { - permissions: { push: true }, + permissions: { + push: true, + }, + clone_url: `https://api.github.local/${owner}/${repo}.git`, }); await t.notThrowsAsync( @@ -543,7 +585,10 @@ test('Verify "assets" is an Array of Object with a path property', async (t) => const fetch = fetchMock .sandbox() .getOnce(`https://api.github.local/repos/${owner}/${repo}`, { - permissions: { push: true }, + permissions: { + push: true, + }, + clone_url: `https://api.github.local/${owner}/${repo}.git`, }); await t.notThrowsAsync( @@ -577,7 +622,10 @@ test('Verify "assets" is an Array of glob Arrays', async (t) => { const fetch = fetchMock .sandbox() .getOnce(`https://api.github.local/repos/${owner}/${repo}`, { - permissions: { push: true }, + permissions: { + push: true, + }, + clone_url: `https://api.github.local/${owner}/${repo}.git`, }); await t.notThrowsAsync( @@ -609,7 +657,10 @@ test('Verify "assets" is an Array of Object with a glob Arrays in path property' const fetch = fetchMock .sandbox() .getOnce(`https://api.github.local/repos/${owner}/${repo}`, { - permissions: { push: true }, + permissions: { + push: true, + }, + clone_url: `https://api.github.local/${owner}/${repo}.git`, }); await t.notThrowsAsync( @@ -643,7 +694,10 @@ test('Verify "labels" is a String', async (t) => { const fetch = fetchMock .sandbox() .getOnce(`https://api.github.local/repos/${owner}/${repo}`, { - permissions: { push: true }, + permissions: { + push: true, + }, + clone_url: `https://api.github.local/${owner}/${repo}.git`, }); await t.notThrowsAsync( @@ -675,7 +729,10 @@ test('Verify "assignees" is a String', async (t) => { const fetch = fetchMock .sandbox() .getOnce(`https://api.github.local/repos/${owner}/${repo}`, { - permissions: { push: true }, + permissions: { + push: true, + }, + clone_url: `https://api.github.local/${owner}/${repo}.git`, }); await t.notThrowsAsync( @@ -707,7 +764,10 @@ test('Verify "addReleases" is a valid string (top)', async (t) => { const fetch = fetchMock .sandbox() .getOnce(`https://api.github.local/repos/${owner}/${repo}`, { - permissions: { push: true }, + permissions: { + push: true, + }, + clone_url: `https://api.github.local/${owner}/${repo}.git`, }); await t.notThrowsAsync( @@ -739,7 +799,10 @@ test('Verify "addReleases" is a valid string (bottom)', async (t) => { const fetch = fetchMock .sandbox() .getOnce(`https://api.github.local/repos/${owner}/${repo}`, { - permissions: { push: true }, + permissions: { + push: true, + }, + clone_url: `https://api.github.local/${owner}/${repo}.git`, }); await t.notThrowsAsync( @@ -771,7 +834,10 @@ test('Verify "addReleases" is valid (false)', async (t) => { const fetch = fetchMock .sandbox() .getOnce(`https://api.github.local/repos/${owner}/${repo}`, { - permissions: { push: true }, + permissions: { + push: true, + }, + clone_url: `https://api.github.local/${owner}/${repo}.git`, }); await t.notThrowsAsync( @@ -803,7 +869,10 @@ test('Verify "draftRelease" is valid (true)', async (t) => { const fetch = fetchMock .sandbox() .getOnce(`https://api.github.local/repos/${owner}/${repo}`, { - permissions: { push: true }, + permissions: { + push: true, + }, + clone_url: `https://api.github.local/${owner}/${repo}.git`, }); await t.notThrowsAsync( @@ -835,7 +904,10 @@ test('Verify "draftRelease" is valid (false)', async (t) => { const fetch = fetchMock .sandbox() .getOnce(`https://api.github.local/repos/${owner}/${repo}`, { - permissions: { push: true }, + permissions: { + push: true, + }, + clone_url: `https://api.github.local/${owner}/${repo}.git`, }); await t.notThrowsAsync( @@ -874,13 +946,65 @@ test("Verify if run in GitHub Action", async (t) => { const labels = ["semantic-release"]; const discussionCategoryName = "Announcements"; + const fetch = fetchMock + .sandbox() + .getOnce(`https://api.github.local/repos/${owner}/${repo}`, { + clone_url: `https://api.github.local/${owner}/${repo}.git`, + }); + await t.notThrowsAsync( verify( { proxy, assets, successComment, failTitle, failComment, labels }, { env, options: { - repositoryUrl: `git+https://othertesturl.com/${owner}/${repo}.git`, + repositoryUrl: `git+https://othertesturl.com:9090/${owner}/${repo}.git`, + }, + logger: t.context.logger, + }, + { + Octokit: TestOctokit.defaults((options) => ({ + ...options, + request: { ...options.request, fetch }, + })), + }, + ), + ); + + t.true(fetch.done()); +}); + +// https://github.com/semantic-release/github/issues/182 +test("Verify if run in GitHub Action and repo is renamed", async (t) => { + const owner = "test_user"; + const repo = "test_repo"; + const env = { + GITHUB_TOKEN: "v1.1234567890123456789012345678901234567890", + GITHUB_ACTION: "Release", + }; + const proxy = "https://localhost"; + const assets = [{ path: "lib/file.js" }, "file.js"]; + const successComment = "Test comment"; + const failTitle = "Test title"; + const failComment = "Test comment"; + const labels = ["semantic-release"]; + const discussionCategoryName = "Announcements"; + + const fetch = fetchMock + .sandbox() + .getOnce(`https://api.github.local/repos/${owner}/${repo}`, { + clone_url: `https://api.github.local/${owner}/${repo}2.git`, + }); + + const { + errors: [error, ...errors], + } = await t.throwsAsync( + verify( + { proxy, assets, successComment, failTitle, failComment, labels }, + { + env, + options: { + repositoryUrl: `git+https://othertesturl.com:9090/${owner}/${repo}.git`, }, logger: t.context.logger, }, @@ -892,6 +1016,54 @@ test("Verify if run in GitHub Action", async (t) => { }, ), ); + + t.is(errors.length, 0); + t.is(error.name, "SemanticReleaseError"); + t.is(error.code, "EMISMATCHGITHUBURL"); + t.true(fetch.done()); +}); + +test("Verify if token is a Github installation token and repo is renamed", async (t) => { + const owner = "test_user"; + const repo = "test_repo"; + const env = { GH_TOKEN: "github_token" }; + + const fetch = fetchMock + .sandbox() + .getOnce(`https://api.github.local/repos/${owner}/${repo}`, { + permissions: { + push: false, + }, + clone_url: `https://api.github.local/${owner}/${repo}2.git`, + }) + .headOnce( + "https://api.github.local/installation/repositories?per_page=1", + 200, + ); + + const { + errors: [error, ...errors], + } = await t.throwsAsync( + verify( + {}, + { + env, + options: { repositoryUrl: `https://github.com/${owner}/${repo}.git` }, + logger: t.context.logger, + }, + { + Octokit: TestOctokit.defaults((options) => ({ + ...options, + request: { ...options.request, fetch }, + })), + }, + ), + ); + + t.is(errors.length, 0); + t.is(error.name, "SemanticReleaseError"); + t.is(error.code, "EMISMATCHGITHUBURL"); + t.true(fetch.done()); }); test("Throw SemanticReleaseError for missing github token", async (t) => { @@ -990,7 +1162,10 @@ test("Throw SemanticReleaseError if token doesn't have the push permission on th const fetch = fetchMock .sandbox() .getOnce(`https://api.github.local/repos/${owner}/${repo}`, { - permissions: { push: false }, + permissions: { + push: false, + }, + clone_url: `https://api.github.local/${owner}/${repo}.git`, }) .headOnce( "https://api.github.local/installation/repositories?per_page=1", @@ -1030,7 +1205,10 @@ test("Do not throw SemanticReleaseError if token doesn't have the push permissio const fetch = fetchMock .sandbox() .getOnce(`https://api.github.local/repos/${owner}/${repo}`, { - permissions: { push: false }, + permissions: { + push: false, + }, + clone_url: `https://api.github.local/${owner}/${repo}.git`, }) .headOnce( "https://api.github.local/installation/repositories?per_page=1", @@ -1091,6 +1269,93 @@ test("Throw SemanticReleaseError if the repository doesn't exist", async (t) => t.true(fetch.done()); }); +const urlFormats = [ + (owner, repo) => `https://github.com/${owner}/${repo}.git`, + (owner, repo) => `git+https://github.com/${owner}/${repo}.git`, + (owner, repo) => `http://github.com/${owner}/${repo}.git`, + (owner, repo) => `git@github.com:${owner}/${repo}.git`, + (owner, repo) => `ssh://git@github.com/${owner}/${repo}.git`, + (owner, repo) => `git://github.com/${owner}/${repo}.git`, +]; + +for (const makeRepositoryUrl of urlFormats) { + for (const make_clone_url of urlFormats) { + const owner = "test_user"; + const repo = "test_repo"; + test(`Don't throw an error if clone_url differs from repositoryUrl but owner/repo is the same -- ${makeRepositoryUrl(owner, repo)} / ${make_clone_url(owner, repo)}`, async (t) => { + const env = { GH_TOKEN: "github_token" }; + + const fetch = fetchMock.sandbox().getOnce( + `https://api.github.local/repos/${owner}/${repo}`, + { + permissions: { push: true }, + clone_url: make_clone_url(owner, repo), + }, + { repeat: 2 }, + ); + + await t.notThrowsAsync( + verify( + {}, + { + env, + options: { + repositoryUrl: makeRepositoryUrl(owner, repo), + }, + logger: t.context.logger, + }, + { + Octokit: TestOctokit.defaults((options) => ({ + ...options, + request: { ...options.request, fetch }, + })), + }, + ), + ); + + t.true(fetch.done()); + }); + + const repo2 = repo + "2"; + test(`Throw SemanticReleaseError if the repository is renamed -- ${makeRepositoryUrl(owner, repo)} / ${make_clone_url(owner, repo2)}`, async (t) => { + const env = { GH_TOKEN: "github_token" }; + + const fetch = fetchMock.sandbox().getOnce( + `https://api.github.local/repos/${owner}/${repo}`, + { + permissions: { push: true }, + clone_url: make_clone_url(owner, repo2), + }, + { repeat: 2 }, + ); + + const { + errors: [error, ...errors], + } = await t.throwsAsync( + verify( + {}, + { + env, + options: { repositoryUrl: makeRepositoryUrl(owner, repo) }, + logger: t.context.logger, + }, + { + Octokit: TestOctokit.defaults((options) => ({ + ...options, + request: { ...options.request, fetch }, + })), + }, + ), + ); + + t.is(errors.length, 0); + t.is(error.name, "SemanticReleaseError"); + t.is(error.code, "EMISMATCHGITHUBURL"); + t.true(fetch.done()); + }); + } +} + test("Throw error if github return any other errors", async (t) => { const owner = "test_user"; const repo = "test_repo"; @@ -1190,7 +1455,10 @@ test('Throw SemanticReleaseError if "assets" option is not a String or an Array const fetch = fetchMock .sandbox() .getOnce(`https://api.github.local/repos/${owner}/${repo}`, { - permissions: { push: true }, + permissions: { + push: true, + }, + clone_url: `https://api.github.local/${owner}/${repo}.git`, }); const { @@ -1227,7 +1495,10 @@ test('Throw SemanticReleaseError if "assets" option is an Array with invalid ele const fetch = fetchMock .sandbox() .getOnce(`https://api.github.local/repos/${owner}/${repo}`, { - permissions: { push: true }, + permissions: { + push: true, + }, + clone_url: `https://api.github.local/${owner}/${repo}.git`, }); const { @@ -1264,7 +1535,10 @@ test('Throw SemanticReleaseError if "assets" option is an Object missing the "pa const fetch = fetchMock .sandbox() .getOnce(`https://api.github.local/repos/${owner}/${repo}`, { - permissions: { push: true }, + permissions: { + push: true, + }, + clone_url: `https://api.github.local/${owner}/${repo}.git`, }); const { @@ -1301,7 +1575,10 @@ test('Throw SemanticReleaseError if "assets" option is an Array with objects mis const fetch = fetchMock .sandbox() .getOnce(`https://api.github.local/repos/${owner}/${repo}`, { - permissions: { push: true }, + permissions: { + push: true, + }, + clone_url: `https://api.github.local/${owner}/${repo}.git`, }); const { @@ -1338,7 +1615,10 @@ test('Throw SemanticReleaseError if "successComment" option is not a String', as const fetch = fetchMock .sandbox() .getOnce(`https://api.github.local/repos/${owner}/${repo}`, { - permissions: { push: true }, + permissions: { + push: true, + }, + clone_url: `https://api.github.local/${owner}/${repo}.git`, }); const { @@ -1375,7 +1655,10 @@ test('Throw SemanticReleaseError if "successComment" option is an empty String', const fetch = fetchMock .sandbox() .getOnce(`https://api.github.local/repos/${owner}/${repo}`, { - permissions: { push: true }, + permissions: { + push: true, + }, + clone_url: `https://api.github.local/${owner}/${repo}.git`, }); const { @@ -1412,7 +1695,10 @@ test('Throw SemanticReleaseError if "successComment" option is a whitespace Stri const fetch = fetchMock .sandbox() .getOnce(`https://api.github.local/repos/${owner}/${repo}`, { - permissions: { push: true }, + permissions: { + push: true, + }, + clone_url: `https://api.github.local/${owner}/${repo}.git`, }); const { @@ -1449,7 +1735,10 @@ test('Throw SemanticReleaseError if "failTitle" option is not a String', async ( const fetch = fetchMock .sandbox() .getOnce(`https://api.github.local/repos/${owner}/${repo}`, { - permissions: { push: true }, + permissions: { + push: true, + }, + clone_url: `https://api.github.local/${owner}/${repo}.git`, }); const { @@ -1486,7 +1775,10 @@ test('Throw SemanticReleaseError if "failTitle" option is an empty String', asyn const fetch = fetchMock .sandbox() .getOnce(`https://api.github.local/repos/${owner}/${repo}`, { - permissions: { push: true }, + permissions: { + push: true, + }, + clone_url: `https://api.github.local/${owner}/${repo}.git`, }); const { @@ -1523,7 +1815,10 @@ test('Throw SemanticReleaseError if "failTitle" option is a whitespace String', const fetch = fetchMock .sandbox() .getOnce(`https://api.github.local/repos/${owner}/${repo}`, { - permissions: { push: true }, + permissions: { + push: true, + }, + clone_url: `https://api.github.local/${owner}/${repo}.git`, }); const { @@ -1560,7 +1855,10 @@ test('Throw SemanticReleaseError if "discussionCategoryName" option is not a Str const fetch = fetchMock .sandbox() .getOnce(`https://api.github.local/repos/${owner}/${repo}`, { - permissions: { push: true }, + permissions: { + push: true, + }, + clone_url: `https://api.github.local/${owner}/${repo}.git`, }); const { @@ -1597,7 +1895,10 @@ test('Throw SemanticReleaseError if "discussionCategoryName" option is an empty const fetch = fetchMock .sandbox() .getOnce(`https://api.github.local/repos/${owner}/${repo}`, { - permissions: { push: true }, + permissions: { + push: true, + }, + clone_url: `https://api.github.local/${owner}/${repo}.git`, }); const { @@ -1634,7 +1935,10 @@ test('Throw SemanticReleaseError if "discussionCategoryName" option is a whitesp const fetch = fetchMock .sandbox() .getOnce(`https://api.github.local/repos/${owner}/${repo}`, { - permissions: { push: true }, + permissions: { + push: true, + }, + clone_url: `https://api.github.local/${owner}/${repo}.git`, }); const { @@ -1671,7 +1975,10 @@ test('Throw SemanticReleaseError if "failComment" option is not a String', async const fetch = fetchMock .sandbox() .getOnce(`https://api.github.local/repos/${owner}/${repo}`, { - permissions: { push: true }, + permissions: { + push: true, + }, + clone_url: `https://api.github.local/${owner}/${repo}.git`, }); const { @@ -1708,7 +2015,10 @@ test('Throw SemanticReleaseError if "failComment" option is an empty String', as const fetch = fetchMock .sandbox() .getOnce(`https://api.github.local/repos/${owner}/${repo}`, { - permissions: { push: true }, + permissions: { + push: true, + }, + clone_url: `https://api.github.local/${owner}/${repo}.git`, }); const { @@ -1745,7 +2055,10 @@ test('Throw SemanticReleaseError if "failComment" option is a whitespace String' const fetch = fetchMock .sandbox() .getOnce(`https://api.github.local/repos/${owner}/${repo}`, { - permissions: { push: true }, + permissions: { + push: true, + }, + clone_url: `https://api.github.local/${owner}/${repo}.git`, }); const { @@ -1782,7 +2095,10 @@ test('Throw SemanticReleaseError if "labels" option is not a String or an Array const fetch = fetchMock .sandbox() .getOnce(`https://api.github.local/repos/${owner}/${repo}`, { - permissions: { push: true }, + permissions: { + push: true, + }, + clone_url: `https://api.github.local/${owner}/${repo}.git`, }); const { @@ -1819,7 +2135,10 @@ test('Throw SemanticReleaseError if "labels" option is an Array with invalid ele const fetch = fetchMock .sandbox() .getOnce(`https://api.github.local/repos/${owner}/${repo}`, { - permissions: { push: true }, + permissions: { + push: true, + }, + clone_url: `https://api.github.local/${owner}/${repo}.git`, }); const { @@ -1856,7 +2175,10 @@ test('Throw SemanticReleaseError if "labels" option is a whitespace String', asy const fetch = fetchMock .sandbox() .getOnce(`https://api.github.local/repos/${owner}/${repo}`, { - permissions: { push: true }, + permissions: { + push: true, + }, + clone_url: `https://api.github.local/${owner}/${repo}.git`, }); const { @@ -1893,7 +2215,10 @@ test('Throw SemanticReleaseError if "assignees" option is not a String or an Arr const fetch = fetchMock .sandbox() .getOnce(`https://api.github.local/repos/${owner}/${repo}`, { - permissions: { push: true }, + permissions: { + push: true, + }, + clone_url: `https://api.github.local/${owner}/${repo}.git`, }); const { @@ -1930,7 +2255,10 @@ test('Throw SemanticReleaseError if "assignees" option is an Array with invalid const fetch = fetchMock .sandbox() .getOnce(`https://api.github.local/repos/${owner}/${repo}`, { - permissions: { push: true }, + permissions: { + push: true, + }, + clone_url: `https://api.github.local/${owner}/${repo}.git`, }); const { @@ -1967,7 +2295,10 @@ test('Throw SemanticReleaseError if "assignees" option is a whitespace String', const fetch = fetchMock .sandbox() .getOnce(`https://api.github.local/repos/${owner}/${repo}`, { - permissions: { push: true }, + permissions: { + push: true, + }, + clone_url: `https://api.github.local/${owner}/${repo}.git`, }); const { @@ -2004,7 +2335,10 @@ test('Throw SemanticReleaseError if "releasedLabels" option is not a String or a const fetch = fetchMock .sandbox() .getOnce(`https://api.github.local/repos/${owner}/${repo}`, { - permissions: { push: true }, + permissions: { + push: true, + }, + clone_url: `https://api.github.local/${owner}/${repo}.git`, }); const { @@ -2041,7 +2375,10 @@ test('Throw SemanticReleaseError if "releasedLabels" option is an Array with inv const fetch = fetchMock .sandbox() .getOnce(`https://api.github.local/repos/${owner}/${repo}`, { - permissions: { push: true }, + permissions: { + push: true, + }, + clone_url: `https://api.github.local/${owner}/${repo}.git`, }); const { @@ -2078,7 +2415,10 @@ test('Throw SemanticReleaseError if "releasedLabels" option is a whitespace Stri const fetch = fetchMock .sandbox() .getOnce(`https://api.github.local/repos/${owner}/${repo}`, { - permissions: { push: true }, + permissions: { + push: true, + }, + clone_url: `https://api.github.local/${owner}/${repo}.git`, }); const { @@ -2115,7 +2455,10 @@ test('Throw SemanticReleaseError if "addReleases" option is not a valid string ( const fetch = fetchMock .sandbox() .getOnce(`https://api.github.local/repos/${owner}/${repo}`, { - permissions: { push: true }, + permissions: { + push: true, + }, + clone_url: `https://api.github.local/${owner}/${repo}.git`, }); const { @@ -2152,7 +2495,10 @@ test('Throw SemanticReleaseError if "addReleases" option is not a valid string ( const fetch = fetchMock .sandbox() .getOnce(`https://api.github.local/repos/${owner}/${repo}`, { - permissions: { push: true }, + permissions: { + push: true, + }, + clone_url: `https://api.github.local/${owner}/${repo}.git`, }); const { @@ -2189,7 +2535,10 @@ test('Throw SemanticReleaseError if "addReleases" option is not a valid string ( const fetch = fetchMock .sandbox() .getOnce(`https://api.github.local/repos/${owner}/${repo}`, { - permissions: { push: true }, + permissions: { + push: true, + }, + clone_url: `https://api.github.local/${owner}/${repo}.git`, }); const { @@ -2226,7 +2575,10 @@ test('Throw SemanticReleaseError if "draftRelease" option is not a valid boolean const fetch = fetchMock .sandbox() .getOnce(`https://api.github.local/repos/${owner}/${repo}`, { - permissions: { push: true }, + permissions: { + push: true, + }, + clone_url: `https://api.github.local/${owner}/${repo}.git`, }); const { @@ -2262,7 +2614,10 @@ test('Throw SemanticReleaseError if "releaseBodyTemplate" option is an empty str const fetch = fetchMock .sandbox() .getOnce(`https://api.github.local/repos/${owner}/${repo}`, { - permissions: { push: true }, + permissions: { + push: true, + }, + clone_url: `https://api.github.local/${owner}/${repo}.git`, }); const { @@ -2298,7 +2653,10 @@ test('Throw SemanticReleaseError if "releaseNameTemplate" option is an empty str const fetch = fetchMock .sandbox() .getOnce(`https://api.github.local/repos/${owner}/${repo}`, { - permissions: { push: true }, + permissions: { + push: true, + }, + clone_url: `https://api.github.local/${owner}/${repo}.git`, }); const {