diff --git a/lib/definitions/errors.js b/lib/definitions/errors.js index 74fa1ce2..3cdbde55 100644 --- a/lib/definitions/errors.js +++ b/lib/definitions/errors.js @@ -178,12 +178,24 @@ If you are using [GitHub Enterprise](https://enterprise.github.com) please make export function EGHNOPERMISSION({ owner, repo }) { return { - message: `The GitHub token doesn't allow to push on the repository ${owner}/${repo}.`, + message: `The GitHub token doesn't allow to push to and maintain the repository ${owner}/${repo}.`, details: `The user associated with the [GitHub token](${linkify( "README.md#github-authentication", - )}) configured in the \`GH_TOKEN\` or \`GITHUB_TOKEN\` environment variable must allows to push to the repository ${owner}/${repo}. + )}) configured in the \`GH_TOKEN\` or \`GITHUB_TOKEN\` environment variable must have permission to push to and maintain the repository ${owner}/${repo}. -Please make sure the GitHub user associated with the token is an [owner](https://help.github.com/articles/permission-levels-for-a-user-account-repository/#owner-access-on-a-repository-owned-by-a-user-account) or a [collaborator](https://help.github.com/articles/permission-levels-for-a-user-account-repository/#collaborator-access-on-a-repository-owned-by-a-user-account) if the repository belong to a user account or has [write permissions](https://help.github.com/articles/managing-team-access-to-an-organization-repository) if the repository [belongs to an organization](https://help.github.com/articles/repository-permission-levels-for-an-organization).`, +Please make sure the GitHub user associated with the token is an [owner](https://help.github.com/articles/permission-levels-for-a-user-account-repository/#owner-access-on-a-repository-owned-by-a-user-account) or a [collaborator](https://help.github.com/articles/permission-levels-for-a-user-account-repository/#collaborator-access-on-a-repository-owned-by-a-user-account) if the repository belongs to a user account or has [write permissions](https://help.github.com/articles/managing-team-access-to-an-organization-repository) if the repository [belongs to an organization](https://help.github.com/articles/repository-permission-levels-for-an-organization).`, + }; +} + +export function EGHNOSCOPE({ scopes }) { + return { + message: `The GitHub token doesn't have the necessary OAuth scopes to write contents, issues, and pull requests.`, + details: `The [GitHub token](${linkify( + "README.md#github-authentication", + )}) configured in the \`GH_TOKEN\` or \`GITHUB_TOKEN\` environment variable must have the correct scopes. +${scopes ? `\nThe token you used has scopes: ${scopes.join(", ")}\n` : ""} +For classic PATs, make sure the token has the \`repo\` scope if the repository is private, or \`public_repo\` scope otherwise. +For fine-grained PATs, make sure the token has the \`content: write\`, \`issues: write\`, and \`pull_requests: write\` scopes on the repository.`, }; } diff --git a/lib/verify.js b/lib/verify.js index a0987c12..39d5d3c2 100644 --- a/lib/verify.js +++ b/lib/verify.js @@ -105,8 +105,21 @@ export default async function verify(pluginConfig, context, { Octokit }) { ); try { const { - data: { permissions, clone_url }, + headers, + data: { private: _private, permissions, clone_url }, } = await octokit.request("GET /repos/{owner}/{repo}", { repo, owner }); + + // GitHub only returns this header if the token is a classic PAT + if (headers?.["x-oauth-scopes"]) { + const scopes = headers["x-oauth-scopes"].split(/\s*,\s*/g); + if ( + !scopes.includes("repo") && + (_private || !scopes.includes("public_repo")) + ) { + errors.push(getError("EGHNOSCOPE", { scopes })); + } + } + // Verify if Repository Name wasn't changed const parsedCloneUrl = parseGithubUrl(clone_url); if ( @@ -122,7 +135,7 @@ export default async function verify(pluginConfig, context, { Octokit }) { // 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 (!env.GITHUB_ACTION && !(permissions?.push && permissions?.maintain)) { // 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 diff --git a/package-lock.json b/package-lock.json index a24d4a65..d9b9c701 100644 --- a/package-lock.json +++ b/package-lock.json @@ -77,9 +77,9 @@ } }, "node_modules/@babel/compat-data": { - "version": "7.25.2", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.25.2.tgz", - "integrity": "sha512-bYcppcpKBvX4znYaPEeFau03bp89ShqNMLs+rmdptMw+heSZh9+z84d2YG+K7cYLbWwzdjtDoW/uqZmPjulClQ==", + "version": "7.25.4", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.25.4.tgz", + "integrity": "sha512-+LGRog6RAsCJrrrg/IO6LGmpphNe5DiK30dGjCoxxeGv49B10/3XYGxPsAwrDlMFcFEvdAUavDT8r9k/hSyQqQ==", "dev": true, "license": "MIT", "engines": { @@ -128,13 +128,13 @@ } }, "node_modules/@babel/generator": { - "version": "7.25.0", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.25.0.tgz", - "integrity": "sha512-3LEEcj3PVW8pW2R1SR1M89g/qrYk/m/mB/tLqn7dn4sbBUQyTqnlod+II2U4dqiGtUmkcnAmkMDralTFZttRiw==", + "version": "7.25.6", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.25.6.tgz", + "integrity": "sha512-VPC82gr1seXOpkjAAKoLhP50vx4vGNlF4msF64dSFq1P8RfB+QAuJWGHPXXPc8QyfVWwwB/TNNU4+ayZmHNbZw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/types": "^7.25.0", + "@babel/types": "^7.25.6", "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25", "jsesc": "^2.5.1" @@ -248,14 +248,14 @@ } }, "node_modules/@babel/helpers": { - "version": "7.25.0", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.25.0.tgz", - "integrity": "sha512-MjgLZ42aCm0oGjJj8CtSM3DB8NOOf8h2l7DCTePJs29u+v7yO/RBX9nShlKMgFnRks/Q4tBAe7Hxnov9VkGwLw==", + "version": "7.25.6", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.25.6.tgz", + "integrity": "sha512-Xg0tn4HcfTijTwfDwYlvVCl43V6h4KyVVX2aEm4qdO/PC6L2YvzLHFdmxhoeSA3eslcE6+ZVXHgWwopXYLNq4Q==", "dev": true, "license": "MIT", "dependencies": { "@babel/template": "^7.25.0", - "@babel/types": "^7.25.0" + "@babel/types": "^7.25.6" }, "engines": { "node": ">=6.9.0" @@ -339,13 +339,13 @@ } }, "node_modules/@babel/parser": { - "version": "7.25.3", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.25.3.tgz", - "integrity": "sha512-iLTJKDbJ4hMvFPgQwwsVoxtHyWpKKPBrxkANrSYewDPaPpT5py5yeVkgPIJ7XYXhndxJpaA3PyALSXQ7u8e/Dw==", + "version": "7.25.6", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.25.6.tgz", + "integrity": "sha512-trGdfBdbD0l1ZPmcJ83eNxB9rbEax4ALFTF7fN386TMYbeCQbyme5cOEXQhbGXKebwGaB/J52w1mrklMcbgy6Q==", "dev": true, "license": "MIT", "dependencies": { - "@babel/types": "^7.25.2" + "@babel/types": "^7.25.6" }, "bin": { "parser": "bin/babel-parser.js" @@ -355,9 +355,9 @@ } }, "node_modules/@babel/runtime": { - "version": "7.25.0", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.25.0.tgz", - "integrity": "sha512-7dRy4DwXwtzBrPbZflqxnvfxLF8kdZXPkhymtDeFoFqE6ldzjQFgYTtYIFARcLEYDrqfBfYcZt1WqFxRoyC9Rw==", + "version": "7.25.6", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.25.6.tgz", + "integrity": "sha512-VBj9MYyDb9tuLq7yzqjgzt6Q+IBQLrGZfdjOekyEirZPHxXWoTSGUTMrpsfi58Up73d13NfYLv8HT9vmznjzhQ==", "dev": true, "license": "MIT", "dependencies": { @@ -383,17 +383,17 @@ } }, "node_modules/@babel/traverse": { - "version": "7.25.3", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.25.3.tgz", - "integrity": "sha512-HefgyP1x754oGCsKmV5reSmtV7IXj/kpaE1XYY+D9G5PvKKoFfSbiS4M77MdjuwlZKDIKFCffq9rPU+H/s3ZdQ==", + "version": "7.25.6", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.25.6.tgz", + "integrity": "sha512-9Vrcx5ZW6UwK5tvqsj0nGpp/XzqthkT0dqIc9g1AdtygFToNtTF67XzYS//dm+SAK9cp3B9R4ZO/46p63SCjlQ==", "dev": true, "license": "MIT", "dependencies": { "@babel/code-frame": "^7.24.7", - "@babel/generator": "^7.25.0", - "@babel/parser": "^7.25.3", + "@babel/generator": "^7.25.6", + "@babel/parser": "^7.25.6", "@babel/template": "^7.25.0", - "@babel/types": "^7.25.2", + "@babel/types": "^7.25.6", "debug": "^4.3.1", "globals": "^11.1.0" }, @@ -402,9 +402,9 @@ } }, "node_modules/@babel/types": { - "version": "7.25.2", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.25.2.tgz", - "integrity": "sha512-YTnYtra7W9e6/oAZEHj0bJehPRUlLH9/fbpT5LfB0NhQXyALCRkRs3zH9v07IYhkgpqX6Z78FnuccZr/l4Fs4Q==", + "version": "7.25.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.25.6.tgz", + "integrity": "sha512-/l42B1qxpG6RdfYf343Uw1vmDjeNhneUXtzhojE7pDgfpEypmRhI6j1kr17XCVv4Cgl9HdAiQY2x0GwKm7rWCw==", "dev": true, "license": "MIT", "dependencies": { @@ -1540,9 +1540,9 @@ } }, "node_modules/@semantic-release/github": { - "version": "10.1.6", - "resolved": "https://registry.npmjs.org/@semantic-release/github/-/github-10.1.6.tgz", - "integrity": "sha512-UTW7hNp6nDeJJWrHcNx8dki95d12WVh++PH98rIr7PQxrZrnjtL0ys/rsAt9tOBTWBaCZdj6797RMLkY9tU+ug==", + "version": "10.1.7", + "resolved": "https://registry.npmjs.org/@semantic-release/github/-/github-10.1.7.tgz", + "integrity": "sha512-QnhP4k1eqzYLz6a4kpWrUQeKJYXqHggveMykvUFbSquq07GF85BXvr/QLhpOD7bpDcmEfL8VnphRA7KT5i9lzQ==", "dev": true, "license": "MIT", "dependencies": { @@ -1856,13 +1856,13 @@ } }, "node_modules/@sinonjs/fake-timers": { - "version": "11.2.2", - "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-11.2.2.tgz", - "integrity": "sha512-G2piCSxQ7oWOxwGSAyFHfPIsyeJGXYtc6mFbnFA+kRXkiEnTl8c/8jul2S329iFBnDI9HGoeWWAZvuvOkZccgw==", + "version": "11.3.1", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-11.3.1.tgz", + "integrity": "sha512-EVJO7nW5M/F5Tur0Rf2z/QoMo+1Ia963RiMtapiQrEWvY0iBUvADo8Beegwjpnle5BHkyHuoxSTW3jF43H1XRA==", "dev": true, "license": "BSD-3-Clause", "dependencies": { - "@sinonjs/commons": "^3.0.0" + "@sinonjs/commons": "^3.0.1" } }, "node_modules/@sinonjs/samsam": { @@ -1888,9 +1888,9 @@ } }, "node_modules/@sinonjs/text-encoding": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.2.tgz", - "integrity": "sha512-sXXKG+uL9IrKqViTtao2Ws6dy0znu9sOaP1di/jKGW1M6VssO8vlpXCQcpZ+jisQ1tTFAC5Jo/EOzFbggBagFQ==", + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.3.tgz", + "integrity": "sha512-DE427ROAphMQzU4ENbliGYrBSYPXF+TtLg9S8vzeA+OF4ZKzoDdzfL8sxuMUGS/lgRhM6j1URSk9ghf7Xo1tyA==", "dev": true, "license": "(Unlicense OR Apache-2.0)" }, @@ -1973,9 +1973,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "22.4.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.4.0.tgz", - "integrity": "sha512-49AbMDwYUz7EXxKU/r7mXOsxwFr4BYbvB7tWYxVuLdb2ibd30ijjXINSMAHiEEZk5PCRBmW1gUeisn2VMKt3cQ==", + "version": "22.5.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.5.2.tgz", + "integrity": "sha512-acJsPTEqYqulZS/Yp/S3GgeE6GZ0qYODUR8aVr/DkhHQ8l9nd4j5x1/ZJy9/gHrRlFMqkO6i0I3E27Alu4jjPg==", "dev": true, "license": "MIT", "optional": true, @@ -2900,9 +2900,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001651", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001651.tgz", - "integrity": "sha512-9Cf+Xv1jJNe1xPZLGuUXLNkE1BoDkqRqYyFJ9TDYSqhduqA4hu4oR9HluGoWYQC/aj8WHjsGVV+bwkh0+tegRg==", + "version": "1.0.30001655", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001655.tgz", + "integrity": "sha512-jRGVy3iSGO5Uutn2owlb5gR6qsGngTw9ZTb4ali9f3glshcNmJ2noam4Mo9zia5P9Dk3jNNydy7vQjuE5dQmfg==", "dev": true, "funding": [ { @@ -3675,9 +3675,9 @@ } }, "node_modules/core-js": { - "version": "3.38.0", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.38.0.tgz", - "integrity": "sha512-XPpwqEodRljce9KswjZShh95qJ1URisBeKCjUdq27YdenkslVe7OO0ZJhlYXAChW7OhXaRLl8AAba7IBfoIHug==", + "version": "3.38.1", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.38.1.tgz", + "integrity": "sha512-OP35aUorbU3Zvlx7pjsFdu1rGNnD4pgw/CWoYzRY3t2EzoVT7shKHY1dlAy3f41cGIO7ZDPQimhGFTlEYkG/Hw==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -4198,9 +4198,9 @@ "license": "MIT" }, "node_modules/electron-to-chromium": { - "version": "1.5.11", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.11.tgz", - "integrity": "sha512-R1CccCDYqndR25CaXFd6hp/u9RaaMcftMkphmvuepXr5b1vfLkRml6aWVeBhXJ7rbevHkKEMJtz8XqPf7ffmew==", + "version": "1.5.13", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.13.tgz", + "integrity": "sha512-lbBcvtIJ4J6sS4tb5TLp1b4LyfCdMkwStzXPyAgVgTRAsep4bvrAGaBOP7ZJtQMNJpSQ9SqG4brWOroNaQtm7Q==", "dev": true, "license": "ISC" }, @@ -4218,9 +4218,9 @@ } }, "node_modules/emoji-regex": { - "version": "10.3.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.3.0.tgz", - "integrity": "sha512-QpLs9D9v9kArv4lfDEgg1X/gN5XLnf/A6l9cs8SPZLRZR3ZkY9+kwIQTxm+fsSej5UMYGE8fdoaZVIBlqG0XTw==", + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz", + "integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==", "dev": true, "license": "MIT" }, @@ -4257,9 +4257,9 @@ } }, "node_modules/env-ci": { - "version": "11.0.0", - "resolved": "https://registry.npmjs.org/env-ci/-/env-ci-11.0.0.tgz", - "integrity": "sha512-apikxMgkipkgTvMdRT9MNqWx5VLOci79F4VBd7Op/7OPjjoanjdAvn6fglMCCEf/1bAh8eOiuEVCUs4V3qP3nQ==", + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/env-ci/-/env-ci-11.1.0.tgz", + "integrity": "sha512-Z8dnwSDbV1XYM9SBF2J0GcNVvmfmfh3a49qddGIROhBoVro6MZVTji15z/sJbQ2ko2ei8n988EU1wzoLU/tF+g==", "dev": true, "license": "MIT", "dependencies": { @@ -4581,9 +4581,9 @@ } }, "node_modules/escalade": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", - "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", "dev": true, "license": "MIT", "engines": { @@ -6267,9 +6267,9 @@ } }, "node_modules/is-core-module": { - "version": "2.15.0", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.0.tgz", - "integrity": "sha512-Dd+Lb2/zvk9SKy1TGCt1wFJFo/MWBPMX5x7KcvLajWTGuomczdQX61PvY5yK6SVACwpoexWo81IfFyoKY2QnTA==", + "version": "2.15.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.1.tgz", + "integrity": "sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==", "dev": true, "license": "MIT", "dependencies": { @@ -7735,9 +7735,9 @@ } }, "node_modules/micromatch": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.7.tgz", - "integrity": "sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q==", + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", "license": "MIT", "dependencies": { "braces": "^3.0.3", @@ -8248,9 +8248,9 @@ } }, "node_modules/node-gyp-build": { - "version": "4.8.1", - "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.1.tgz", - "integrity": "sha512-OSs33Z9yWr148JZcbZd5WiAXhh/n9z8TxQcdMhIOlpN9AhWpLfvVFO73+m77bBABQMaY9XSvIa+qk0jlI7Gcaw==", + "version": "4.8.2", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.2.tgz", + "integrity": "sha512-IRUxE4BVsHWXkV/SFOut4qTlagw2aM8T5/vnTsmrHJvVoKueJHRc/JaFND7QDDc61kLYUJ6qlZM3sqTSyx2dTw==", "dev": true, "license": "MIT", "bin": { @@ -8467,9 +8467,9 @@ } }, "node_modules/npm": { - "version": "10.8.2", - "resolved": "https://registry.npmjs.org/npm/-/npm-10.8.2.tgz", - "integrity": "sha512-x/AIjFIKRllrhcb48dqUNAAZl0ig9+qMuN91RpZo3Cb2+zuibfh+KISl6+kVVyktDz230JKc208UkQwwMqyB+w==", + "version": "10.8.3", + "resolved": "https://registry.npmjs.org/npm/-/npm-10.8.3.tgz", + "integrity": "sha512-0IQlyAYvVtQ7uOhDFYZCGK8kkut2nh8cpAdA9E6FvRSJaTgtZRZgNjlC5ZCct//L73ygrpY93CxXpRJDtNqPVg==", "bundleDependencies": [ "@isaacs/string-locale-compare", "@npmcli/arborist", @@ -8562,13 +8562,13 @@ "@sigstore/tuf": "^2.3.4", "abbrev": "^2.0.0", "archy": "~1.0.0", - "cacache": "^18.0.3", + "cacache": "^18.0.4", "chalk": "^5.3.0", "ci-info": "^4.0.0", "cli-columns": "^4.0.0", "fastest-levenshtein": "^1.0.16", "fs-minipass": "^3.0.3", - "glob": "^10.4.2", + "glob": "^10.4.5", "graceful-fs": "^4.2.11", "hosted-git-info": "^7.0.2", "ini": "^4.1.3", @@ -8577,7 +8577,7 @@ "json-parse-even-better-errors": "^3.0.2", "libnpmaccess": "^8.0.6", "libnpmdiff": "^6.1.4", - "libnpmexec": "^8.1.3", + "libnpmexec": "^8.1.4", "libnpmfund": "^5.0.12", "libnpmhook": "^10.0.5", "libnpmorg": "^6.0.6", @@ -8591,12 +8591,12 @@ "minipass": "^7.1.1", "minipass-pipeline": "^1.2.4", "ms": "^2.1.2", - "node-gyp": "^10.1.0", + "node-gyp": "^10.2.0", "nopt": "^7.2.1", "normalize-package-data": "^6.0.2", "npm-audit-report": "^5.0.0", "npm-install-checks": "^6.3.0", - "npm-package-arg": "^11.0.2", + "npm-package-arg": "^11.0.3", "npm-pick-manifest": "^9.1.0", "npm-profile": "^10.0.0", "npm-registry-fetch": "^17.1.0", @@ -8607,7 +8607,7 @@ "proc-log": "^4.2.0", "qrcode-terminal": "^0.12.0", "read": "^3.0.1", - "semver": "^7.6.2", + "semver": "^7.6.3", "spdx-expression-parse": "^4.0.0", "ssri": "^10.0.6", "supports-color": "^9.4.0", @@ -9463,7 +9463,7 @@ } }, "node_modules/npm/node_modules/cacache": { - "version": "18.0.3", + "version": "18.0.4", "dev": true, "inBundle": true, "license": "ISC", @@ -9630,7 +9630,7 @@ } }, "node_modules/npm/node_modules/debug": { - "version": "4.3.5", + "version": "4.3.6", "dev": true, "inBundle": true, "license": "MIT", @@ -9714,7 +9714,7 @@ } }, "node_modules/npm/node_modules/foreground-child": { - "version": "3.2.1", + "version": "3.3.0", "dev": true, "inBundle": true, "license": "ISC", @@ -9742,7 +9742,7 @@ } }, "node_modules/npm/node_modules/glob": { - "version": "10.4.2", + "version": "10.4.5", "dev": true, "inBundle": true, "license": "ISC", @@ -9757,9 +9757,6 @@ "bin": { "glob": "dist/esm/bin.mjs" }, - "engines": { - "node": ">=16 || 14 >=14.18" - }, "funding": { "url": "https://github.com/sponsors/isaacs" } @@ -9943,16 +9940,13 @@ "license": "ISC" }, "node_modules/npm/node_modules/jackspeak": { - "version": "3.4.0", + "version": "3.4.3", "dev": true, "inBundle": true, "license": "BlueOak-1.0.0", "dependencies": { "@isaacs/cliui": "^8.0.2" }, - "engines": { - "node": ">=14" - }, "funding": { "url": "https://github.com/sponsors/isaacs" }, @@ -10038,7 +10032,7 @@ } }, "node_modules/npm/node_modules/libnpmexec": { - "version": "8.1.3", + "version": "8.1.4", "dev": true, "inBundle": true, "license": "ISC", @@ -10172,13 +10166,10 @@ } }, "node_modules/npm/node_modules/lru-cache": { - "version": "10.2.2", + "version": "10.4.3", "dev": true, "inBundle": true, - "license": "ISC", - "engines": { - "node": "14 || >=16.14" - } + "license": "ISC" }, "node_modules/npm/node_modules/make-fetch-happen": { "version": "13.0.1", @@ -10390,7 +10381,7 @@ } }, "node_modules/npm/node_modules/node-gyp": { - "version": "10.1.0", + "version": "10.2.0", "dev": true, "inBundle": true, "license": "MIT", @@ -10401,9 +10392,9 @@ "graceful-fs": "^4.2.6", "make-fetch-happen": "^13.0.0", "nopt": "^7.0.0", - "proc-log": "^3.0.0", + "proc-log": "^4.1.0", "semver": "^7.3.5", - "tar": "^6.1.2", + "tar": "^6.2.1", "which": "^4.0.0" }, "bin": { @@ -10413,15 +10404,6 @@ "node": "^16.14.0 || >=18.0.0" } }, - "node_modules/npm/node_modules/node-gyp/node_modules/proc-log": { - "version": "3.0.0", - "dev": true, - "inBundle": true, - "license": "ISC", - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, "node_modules/npm/node_modules/nopt": { "version": "7.2.1", "dev": true, @@ -10494,7 +10476,7 @@ } }, "node_modules/npm/node_modules/npm-package-arg": { - "version": "11.0.2", + "version": "11.0.3", "dev": true, "inBundle": true, "license": "ISC", @@ -10668,7 +10650,7 @@ } }, "node_modules/npm/node_modules/postcss-selector-parser": { - "version": "6.1.0", + "version": "6.1.2", "dev": true, "inBundle": true, "license": "MIT", @@ -10806,7 +10788,7 @@ "optional": true }, "node_modules/npm/node_modules/semver": { - "version": "7.6.2", + "version": "7.6.3", "dev": true, "inBundle": true, "license": "ISC", @@ -12645,9 +12627,9 @@ } }, "node_modules/read-package-up/node_modules/type-fest": { - "version": "4.25.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.25.0.tgz", - "integrity": "sha512-bRkIGlXsnGBRBQRAY56UXBm//9qH4bmJfFvq83gSz41N282df+fjy8ofcEgc1sM8geNt5cl6mC2g9Fht1cs8Aw==", + "version": "4.26.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.26.0.tgz", + "integrity": "sha512-OduNjVJsFbifKb57UqZ2EMP1i4u64Xwow3NYXUtBbD4vIwJdQd4+xl8YDou1dlm4DVrtwT/7Ky8z8WyCULVfxw==", "dev": true, "license": "(MIT OR CC0-1.0)", "engines": { @@ -12731,9 +12713,9 @@ } }, "node_modules/read-pkg/node_modules/type-fest": { - "version": "4.25.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.25.0.tgz", - "integrity": "sha512-bRkIGlXsnGBRBQRAY56UXBm//9qH4bmJfFvq83gSz41N282df+fjy8ofcEgc1sM8geNt5cl6mC2g9Fht1cs8Aw==", + "version": "4.26.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.26.0.tgz", + "integrity": "sha512-OduNjVJsFbifKb57UqZ2EMP1i4u64Xwow3NYXUtBbD4vIwJdQd4+xl8YDou1dlm4DVrtwT/7Ky8z8WyCULVfxw==", "dev": true, "license": "(MIT OR CC0-1.0)", "engines": { @@ -13662,9 +13644,9 @@ } }, "node_modules/spdx-license-ids": { - "version": "3.0.18", - "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.18.tgz", - "integrity": "sha512-xxRs31BqRYHwiMzudOrpSiHtZ8i/GeionCBDSilhYRj+9gIcI8wCZTlXZKu9vZIVqViP3dcp9qE5G6AlIaD+TQ==", + "version": "3.0.20", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.20.tgz", + "integrity": "sha512-jg25NiDV/1fLtSgEgyvVyDunvaNHbuwF9lfNV17gSmPFAlYzdfNBlLtLzXTevwkPj7DhGbmN9VnmJIgLnhvaBw==", "dev": true, "license": "CC0-1.0" }, @@ -14075,9 +14057,9 @@ } }, "node_modules/supports-hyperlinks": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-3.0.0.tgz", - "integrity": "sha512-QBDPHyPQDRTy9ku4URNGY5Lah8PAaXs6tAAwp55sL5WCsSW7GIfdf6W5ixfziW+t7wh3GVvHyHHyQ1ESsoRvaA==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-3.1.0.tgz", + "integrity": "sha512-2rn0BZ+/f7puLOHZm1HOJfwBggfaHXUpPUSSG/SWM4TWp5KCfmNYwnC3hruy2rZlMnmWZ+QAGpZfchu3f3695A==", "dev": true, "license": "MIT", "dependencies": { @@ -14086,6 +14068,9 @@ }, "engines": { "node": ">=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/table": { @@ -14568,9 +14553,9 @@ } }, "node_modules/tslib": { - "version": "2.6.3", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz", - "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==", + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz", + "integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==", "dev": true, "license": "0BSD" }, @@ -14823,9 +14808,9 @@ } }, "node_modules/uglify-js": { - "version": "3.19.2", - "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.2.tgz", - "integrity": "sha512-S8KA6DDI47nQXJSi2ctQ629YzwOVs+bQML6DAtvy0wgNdpi+0ySpQK0g2pxBq2xfF2z3YCscu7NNA8nXT9PlIQ==", + "version": "3.19.3", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz", + "integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==", "dev": true, "license": "BSD-2-Clause", "optional": true, @@ -14853,9 +14838,9 @@ } }, "node_modules/undici-types": { - "version": "6.19.6", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.6.tgz", - "integrity": "sha512-e/vggGopEfTKSvj4ihnOLTsqhrKRN3LeO6qSN/GxohhuRv8qH9bNQ4B8W7e/vFL+0XTnmHPB4/kegunZGA4Org==", + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", "dev": true, "license": "MIT", "optional": true diff --git a/package.json b/package.json index 812eb107..f9f4f32a 100644 --- a/package.json +++ b/package.json @@ -127,5 +127,5 @@ "github>semantic-release/.github:renovate-config" ] }, - "packageManager": "npm@10.8.2" + "packageManager": "npm@10.8.3" } diff --git a/test/integration.test.js b/test/integration.test.js index 68afd4a9..2cf098b4 100644 --- a/test/integration.test.js +++ b/test/integration.test.js @@ -29,6 +29,7 @@ test("Verify GitHub auth", async (t) => { .getOnce(`https://api.github.local/repos/${owner}/${repo}`, { permissions: { push: true, + maintain: true, }, clone_url: `https://api.github.local/${owner}/${repo}.git`, }); @@ -49,6 +50,43 @@ test("Verify GitHub auth", async (t) => { t.true(fetch.done()); }); +test("Throws when GitHub user lacks maintain permission", async (t) => { + const owner = "test_user"; + const repo = "test_repo"; + const env = { GITHUB_TOKEN: "github_token" }; + const options = { + repositoryUrl: `git+https://othertesturl.com/${owner}/${repo}.git`, + }; + + const fetch = fetchMock + .sandbox() + .getOnce(`https://api.github.local/repos/${owner}/${repo}`, { + permissions: { + push: true, + maintain: false, + }, + clone_url: `https://api.github.local/${owner}/${repo}.git`, + }); + + const { + errors: [error], + } = await t.throwsAsync( + t.context.m.verifyConditions( + {}, + { cwd, env, options, logger: t.context.logger }, + { + Octokit: TestOctokit.defaults((options) => ({ + ...options, + request: { ...options.request, fetch }, + })), + }, + ), + ); + + t.is(error.code, "EGHNOPERMISSION"); + t.true(fetch.done()); +}); + test("Verify GitHub auth with publish options", async (t) => { const owner = "test_user"; const repo = "test_repo"; @@ -62,6 +100,7 @@ test("Verify GitHub auth with publish options", async (t) => { .get(`https://api.github.local/repos/${owner}/${repo}`, { permissions: { push: true, + maintain: true, }, clone_url: `https://api.github.local/${owner}/${repo}.git`, }); @@ -102,6 +141,7 @@ test("Verify GitHub auth and assets config", async (t) => { .getOnce(`https://api.github.local/repos/${owner}/${repo}`, { permissions: { push: true, + maintain: true, }, clone_url: `https://api.github.local/${owner}/${repo}.git`, }); @@ -208,6 +248,7 @@ test("Publish a release with an array of assets", async (t) => { .getOnce(`https://api.github.local/repos/${owner}/${repo}`, { permissions: { push: true, + maintain: true, }, clone_url: `https://api.github.local/${owner}/${repo}.git`, }) @@ -303,6 +344,7 @@ test("Publish a release with release information in assets", async (t) => { .getOnce(`https://api.github.local/repos/${owner}/${repo}`, { permissions: { push: true, + maintain: true, }, clone_url: `https://api.github.local/${owner}/${repo}.git`, }) @@ -376,6 +418,7 @@ test("Update a release", async (t) => { .getOnce(`https://api.github.local/repos/${owner}/${repo}`, { permissions: { push: true, + maintain: true, }, clone_url: `https://api.github.local/${owner}/${repo}.git`, }) @@ -442,7 +485,10 @@ test("Comment and add labels on PR included in the releases", async (t) => { .get( `https://api.github.local/repos/${owner}/${repo}`, { - permissions: { push: true }, + permissions: { + push: true, + maintain: true, + }, full_name: `${owner}/${repo}`, clone_url: `htttps://api.github.local/${owner}/${repo}.git`, }, @@ -550,7 +596,10 @@ test("Open a new issue with the list of errors", async (t) => { .get( `https://api.github.local/repos/${owner}/${repo}`, { - permissions: { push: true }, + permissions: { + push: true, + maintain: true, + }, full_name: `${owner}/${repo}`, clone_url: `htttps://api.github.local/${owner}/${repo}.git`, }, @@ -645,7 +694,10 @@ test("Verify, release and notify success", async (t) => { .get( `https://api.github.local/repos/${owner}/${repo}`, { - permissions: { push: true }, + permissions: { + push: true, + maintain: true, + }, full_name: `${owner}/${repo}`, clone_url: `htttps://api.github.local/${owner}/${repo}.git`, }, @@ -811,7 +863,10 @@ test("Verify, update release and notify success", async (t) => { .get( `https://api.github.local/repos/${owner}/${repo}`, { - permissions: { push: true }, + permissions: { + push: true, + maintain: true, + }, full_name: `${owner}/${repo}`, clone_url: `htttps://api.github.local/${owner}/${repo}.git`, }, @@ -949,7 +1004,10 @@ test("Verify and notify failure", async (t) => { .get( `https://api.github.local/repos/${owner}/${repo}`, { - permissions: { push: true }, + permissions: { + push: true, + maintain: true, + }, full_name: `${owner}/${repo}`, clone_url: `htttps://api.github.local/${owner}/${repo}.git`, }, diff --git a/test/verify.test.js b/test/verify.test.js index 495c7b1b..90c92cd3 100644 --- a/test/verify.test.js +++ b/test/verify.test.js @@ -15,7 +15,7 @@ test.beforeEach((t) => { t.context.logger = { log: t.context.log, error: t.context.error }; }); -test("Verify package, token and repository access", async (t) => { +test("Verify package, token and repository access for private repo with token scopes: repo", async (t) => { const owner = "test_user"; const repo = "test_repo"; const env = { GH_TOKEN: "github_token" }; @@ -30,10 +30,131 @@ test("Verify package, token and repository access", async (t) => { const fetch = fetchMock .sandbox() .getOnce(`https://api.github.local/repos/${owner}/${repo}`, { - permissions: { - push: true, + headers: { + "x-oauth-scopes": "repo", + }, + body: { + private: true, + permissions: { + push: true, + maintain: true, + }, + clone_url: `https://api.github.local/${owner}/${repo}.git`, + }, + }); + + await t.notThrowsAsync( + verify( + { + proxy, + assets, + successComment, + failTitle, + failComment, + labels, + discussionCategoryName, + }, + { + env, + options: { + repositoryUrl: `git+https://othertesturl.com/${owner}/${repo}.git`, + }, + logger: t.context.logger, + }, + { + Octokit: TestOctokit.defaults((options) => ({ + ...options, + request: { ...options.request, fetch }, + })), + }, + ), + ); + t.true(fetch.done()); +}); + +test("Verify package, token and repository access for public repo with token scopes: repo", async (t) => { + const owner = "test_user"; + const repo = "test_repo"; + const env = { GH_TOKEN: "github_token" }; + 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}`, { + headers: { + "x-oauth-scopes": "repo", + }, + body: { + private: false, + permissions: { + push: true, + maintain: true, + }, + clone_url: `https://api.github.local/${owner}/${repo}.git`, + }, + }); + + await t.notThrowsAsync( + verify( + { + proxy, + assets, + successComment, + failTitle, + failComment, + labels, + discussionCategoryName, + }, + { + env, + options: { + repositoryUrl: `git+https://othertesturl.com/${owner}/${repo}.git`, + }, + logger: t.context.logger, + }, + { + Octokit: TestOctokit.defaults((options) => ({ + ...options, + request: { ...options.request, fetch }, + })), + }, + ), + ); + t.true(fetch.done()); +}); + +test("Verify package, token and repository access for public repo with token scopes: public_repo", async (t) => { + const owner = "test_user"; + const repo = "test_repo"; + const env = { GH_TOKEN: "github_token" }; + 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}`, { + headers: { + "x-oauth-scopes": "public_repo", + }, + body: { + private: false, + permissions: { + push: true, + maintain: true, + }, + clone_url: `https://api.github.local/${owner}/${repo}.git`, }, - clone_url: `https://api.github.local/${owner}/${repo}.git`, }); await t.notThrowsAsync( @@ -82,6 +203,7 @@ test('Verify package, token and repository access with "proxy", "asset", "discus .getOnce(`https://api.github.local/repos/${owner}/${repo}`, { permissions: { push: true, + maintain: true, }, clone_url: `https://api.github.local/${owner}/${repo}.git`, }); @@ -127,6 +249,7 @@ test("Verify package, token and repository access and custom URL with prefix", a .getOnce(`https://othertesturl.com:9090/prefix/repos/${owner}/${repo}`, { permissions: { push: true, + maintain: true, }, clone_url: `https://api.github.local/${owner}/${repo}.git`, }); @@ -168,6 +291,7 @@ test("Verify package, token and repository access and custom URL without prefix" .getOnce(`https://othertesturl.com:9090/repos/${owner}/${repo}`, { permissions: { push: true, + maintain: true, }, clone_url: `https://api.github.local/${owner}/${repo}.git`, }); @@ -209,6 +333,7 @@ test("Verify package, token and repository access and shorthand repositoryUrl UR .getOnce(`https://othertesturl.com:9090/repos/${owner}/${repo}`, { permissions: { push: true, + maintain: true, }, clone_url: `https://api.github.local/${owner}/${repo}.git`, }); @@ -251,6 +376,7 @@ test("Verify package, token and repository with environment variables", async (t .getOnce(`https://othertesturl.com:443/prefix/repos/${owner}/${repo}`, { permissions: { push: true, + maintain: true, }, clone_url: `htttps://api.github.local/${owner}/${repo}.git`, }); @@ -295,6 +421,7 @@ test("Verify package, token and repository access with alternative environment v .getOnce(`https://othertesturl.com:443/prefix/repos/${owner}/${repo}`, { permissions: { push: true, + maintain: true, }, clone_url: `htttps://api.github.local/${owner}/${repo}.git`, }); @@ -332,6 +459,7 @@ test("Verify package, token and repository access with custom API URL", async (t .getOnce(`https://api.othertesturl.com:9090/repos/${owner}/${repo}`, { permissions: { push: true, + maintain: true, }, clone_url: `htttps://api.github.local/${owner}/${repo}.git`, }); @@ -374,6 +502,7 @@ test("Verify package, token and repository access with API URL in environment va .getOnce(`https://api.othertesturl.com:443/repos/${owner}/${repo}`, { permissions: { push: true, + maintain: true, }, clone_url: `htttps://api.github.local/${owner}/${repo}.git`, }); @@ -410,6 +539,7 @@ test('Verify "proxy" is a String', async (t) => { .getOnce(`https://api.github.local/repos/${owner}/${repo}`, { permissions: { push: true, + maintain: true, }, clone_url: `https://api.github.local/${owner}/${repo}.git`, }); @@ -445,6 +575,7 @@ test('Verify "proxy" is an object with "host" and "port" properties', async (t) .getOnce(`https://api.github.local/repos/${owner}/${repo}`, { permissions: { push: true, + maintain: true, }, clone_url: `https://api.github.local/${owner}/${repo}.git`, }); @@ -482,6 +613,7 @@ test('Verify "proxy" is a Boolean set to false', async (t) => { .getOnce(`https://api.github.local/repos/${owner}/${repo}`, { permissions: { push: true, + maintain: true, }, clone_url: `https://api.github.local/${owner}/${repo}.git`, }); @@ -517,6 +649,7 @@ test('Verify "assets" is a String', async (t) => { .getOnce(`https://api.github.local/repos/${owner}/${repo}`, { permissions: { push: true, + maintain: true, }, clone_url: `https://api.github.local/${owner}/${repo}.git`, }); @@ -552,6 +685,7 @@ test('Verify "assets" is an Object with a path property', async (t) => { .getOnce(`https://api.github.local/repos/${owner}/${repo}`, { permissions: { push: true, + maintain: true, }, clone_url: `https://api.github.local/${owner}/${repo}.git`, }); @@ -587,6 +721,7 @@ test('Verify "assets" is an Array of Object with a path property', async (t) => .getOnce(`https://api.github.local/repos/${owner}/${repo}`, { permissions: { push: true, + maintain: true, }, clone_url: `https://api.github.local/${owner}/${repo}.git`, }); @@ -624,6 +759,7 @@ test('Verify "assets" is an Array of glob Arrays', async (t) => { .getOnce(`https://api.github.local/repos/${owner}/${repo}`, { permissions: { push: true, + maintain: true, }, clone_url: `https://api.github.local/${owner}/${repo}.git`, }); @@ -659,6 +795,7 @@ test('Verify "assets" is an Array of Object with a glob Arrays in path property' .getOnce(`https://api.github.local/repos/${owner}/${repo}`, { permissions: { push: true, + maintain: true, }, clone_url: `https://api.github.local/${owner}/${repo}.git`, }); @@ -696,6 +833,7 @@ test('Verify "labels" is a String', async (t) => { .getOnce(`https://api.github.local/repos/${owner}/${repo}`, { permissions: { push: true, + maintain: true, }, clone_url: `https://api.github.local/${owner}/${repo}.git`, }); @@ -731,6 +869,7 @@ test('Verify "assignees" is a String', async (t) => { .getOnce(`https://api.github.local/repos/${owner}/${repo}`, { permissions: { push: true, + maintain: true, }, clone_url: `https://api.github.local/${owner}/${repo}.git`, }); @@ -766,6 +905,7 @@ test('Verify "addReleases" is a valid string (top)', async (t) => { .getOnce(`https://api.github.local/repos/${owner}/${repo}`, { permissions: { push: true, + maintain: true, }, clone_url: `https://api.github.local/${owner}/${repo}.git`, }); @@ -801,6 +941,7 @@ test('Verify "addReleases" is a valid string (bottom)', async (t) => { .getOnce(`https://api.github.local/repos/${owner}/${repo}`, { permissions: { push: true, + maintain: true, }, clone_url: `https://api.github.local/${owner}/${repo}.git`, }); @@ -836,6 +977,7 @@ test('Verify "addReleases" is valid (false)', async (t) => { .getOnce(`https://api.github.local/repos/${owner}/${repo}`, { permissions: { push: true, + maintain: true, }, clone_url: `https://api.github.local/${owner}/${repo}.git`, }); @@ -871,6 +1013,7 @@ test('Verify "draftRelease" is valid (true)', async (t) => { .getOnce(`https://api.github.local/repos/${owner}/${repo}`, { permissions: { push: true, + maintain: true, }, clone_url: `https://api.github.local/${owner}/${repo}.git`, }); @@ -906,6 +1049,7 @@ test('Verify "draftRelease" is valid (false)', async (t) => { .getOnce(`https://api.github.local/repos/${owner}/${repo}`, { permissions: { push: true, + maintain: true, }, clone_url: `https://api.github.local/${owner}/${repo}.git`, }); @@ -1154,7 +1298,99 @@ test("Throw SemanticReleaseError for invalid repositoryUrl", async (t) => { t.is(error.code, "EINVALIDGITHUBURL"); }); -test("Throw SemanticReleaseError if token doesn't have the push permission on the repository and it's not a Github installation token", async (t) => { +test("Throw SemanticReleaseError if token doesn't have the repo or public_repo scope on a public repository", 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}`, { + headers: { + "x-oauth-scopes": "repo:status, repo_deployment", + }, + body: { + private: false, + permissions: { + push: true, + maintain: true, + }, + clone_url: `https://api.github.local/${owner}/${repo}.git`, + }, + }); + + 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, "EGHNOSCOPE"); + t.true(fetch.done()); +}); + +test("Throw SemanticReleaseError if token doesn't have the repo scope on a private repository", 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}`, { + headers: { + "x-oauth-scopes": "repo:status, repo_deployment", + }, + body: { + private: true, + permissions: { + push: true, + maintain: true, + }, + clone_url: `https://api.github.local/${owner}/${repo}.git`, + }, + }); + + 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, "EGHNOSCOPE"); + t.true(fetch.done()); +}); + +test("Throw SemanticReleaseError if user doesn't have the push permission on the repository and it's not a Github installation token", async (t) => { const owner = "test_user"; const repo = "test_repo"; const env = { GH_TOKEN: "github_token" }; @@ -1164,6 +1400,7 @@ test("Throw SemanticReleaseError if token doesn't have the push permission on th .getOnce(`https://api.github.local/repos/${owner}/${repo}`, { permissions: { push: false, + maintain: true, }, clone_url: `https://api.github.local/${owner}/${repo}.git`, }) @@ -1197,7 +1434,51 @@ test("Throw SemanticReleaseError if token doesn't have the push permission on th t.true(fetch.done()); }); -test("Do not throw SemanticReleaseError if token doesn't have the push permission but it is a Github installation token", async (t) => { +test("Throw SemanticReleaseError if user doesn't have the maintain permission on the repository and it's not a Github installation token", 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: true, + maintain: false, + }, + clone_url: `https://api.github.local/${owner}/${repo}.git`, + }) + .headOnce( + "https://api.github.local/installation/repositories?per_page=1", + 403, + ); + + 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, "EGHNOPERMISSION"); + t.true(fetch.done()); +}); + +test("Do not throw SemanticReleaseError if user doesn't have the push permission but it is a Github installation token", async (t) => { const owner = "test_user"; const repo = "test_repo"; const env = { GH_TOKEN: "github_token" }; @@ -1275,7 +1556,7 @@ test(`Don't throw an error if owner/repo only differs in case`, async (t) => { const fetch = fetchMock.sandbox().getOnce( `https://api.github.local/repos/org/foo`, { - permissions: { push: true }, + permissions: { push: true, maintain: true }, clone_url: `https://github.com/ORG/FOO.git`, }, { repeat: 2 }, @@ -1322,7 +1603,10 @@ for (const makeRepositoryUrl of urlFormats) { const fetch = fetchMock.sandbox().getOnce( `https://api.github.local/repos/${owner}/${repo}`, { - permissions: { push: true }, + permissions: { + push: true, + maintain: true, + }, clone_url: make_clone_url(owner, repo), }, { repeat: 2 }, @@ -1357,7 +1641,10 @@ for (const makeRepositoryUrl of urlFormats) { const fetch = fetchMock.sandbox().getOnce( `https://api.github.local/repos/${owner}/${repo}`, { - permissions: { push: true }, + permissions: { + push: true, + maintain: true, + }, clone_url: make_clone_url(owner, repo2), }, { repeat: 2 }, @@ -1491,6 +1778,7 @@ test('Throw SemanticReleaseError if "assets" option is not a String or an Array .getOnce(`https://api.github.local/repos/${owner}/${repo}`, { permissions: { push: true, + maintain: true, }, clone_url: `https://api.github.local/${owner}/${repo}.git`, }); @@ -1531,6 +1819,7 @@ test('Throw SemanticReleaseError if "assets" option is an Array with invalid ele .getOnce(`https://api.github.local/repos/${owner}/${repo}`, { permissions: { push: true, + maintain: true, }, clone_url: `https://api.github.local/${owner}/${repo}.git`, }); @@ -1571,6 +1860,7 @@ test('Throw SemanticReleaseError if "assets" option is an Object missing the "pa .getOnce(`https://api.github.local/repos/${owner}/${repo}`, { permissions: { push: true, + maintain: true, }, clone_url: `https://api.github.local/${owner}/${repo}.git`, }); @@ -1611,6 +1901,7 @@ test('Throw SemanticReleaseError if "assets" option is an Array with objects mis .getOnce(`https://api.github.local/repos/${owner}/${repo}`, { permissions: { push: true, + maintain: true, }, clone_url: `https://api.github.local/${owner}/${repo}.git`, }); @@ -1651,6 +1942,7 @@ test('Throw SemanticReleaseError if "successComment" option is not a String', as .getOnce(`https://api.github.local/repos/${owner}/${repo}`, { permissions: { push: true, + maintain: true, }, clone_url: `https://api.github.local/${owner}/${repo}.git`, }); @@ -1691,6 +1983,7 @@ test('Throw SemanticReleaseError if "successComment" option is an empty String', .getOnce(`https://api.github.local/repos/${owner}/${repo}`, { permissions: { push: true, + maintain: true, }, clone_url: `https://api.github.local/${owner}/${repo}.git`, }); @@ -1731,6 +2024,7 @@ test('Throw SemanticReleaseError if "successComment" option is a whitespace Stri .getOnce(`https://api.github.local/repos/${owner}/${repo}`, { permissions: { push: true, + maintain: true, }, clone_url: `https://api.github.local/${owner}/${repo}.git`, }); @@ -1771,6 +2065,7 @@ test('Throw SemanticReleaseError if "failTitle" option is not a String', async ( .getOnce(`https://api.github.local/repos/${owner}/${repo}`, { permissions: { push: true, + maintain: true, }, clone_url: `https://api.github.local/${owner}/${repo}.git`, }); @@ -1811,6 +2106,7 @@ test('Throw SemanticReleaseError if "failTitle" option is an empty String', asyn .getOnce(`https://api.github.local/repos/${owner}/${repo}`, { permissions: { push: true, + maintain: true, }, clone_url: `https://api.github.local/${owner}/${repo}.git`, }); @@ -1851,6 +2147,7 @@ test('Throw SemanticReleaseError if "failTitle" option is a whitespace String', .getOnce(`https://api.github.local/repos/${owner}/${repo}`, { permissions: { push: true, + maintain: true, }, clone_url: `https://api.github.local/${owner}/${repo}.git`, }); @@ -1891,6 +2188,7 @@ test('Throw SemanticReleaseError if "discussionCategoryName" option is not a Str .getOnce(`https://api.github.local/repos/${owner}/${repo}`, { permissions: { push: true, + maintain: true, }, clone_url: `https://api.github.local/${owner}/${repo}.git`, }); @@ -1931,6 +2229,7 @@ test('Throw SemanticReleaseError if "discussionCategoryName" option is an empty .getOnce(`https://api.github.local/repos/${owner}/${repo}`, { permissions: { push: true, + maintain: true, }, clone_url: `https://api.github.local/${owner}/${repo}.git`, }); @@ -1971,6 +2270,7 @@ test('Throw SemanticReleaseError if "discussionCategoryName" option is a whitesp .getOnce(`https://api.github.local/repos/${owner}/${repo}`, { permissions: { push: true, + maintain: true, }, clone_url: `https://api.github.local/${owner}/${repo}.git`, }); @@ -2011,6 +2311,7 @@ test('Throw SemanticReleaseError if "failComment" option is not a String', async .getOnce(`https://api.github.local/repos/${owner}/${repo}`, { permissions: { push: true, + maintain: true, }, clone_url: `https://api.github.local/${owner}/${repo}.git`, }); @@ -2051,6 +2352,7 @@ test('Throw SemanticReleaseError if "failComment" option is an empty String', as .getOnce(`https://api.github.local/repos/${owner}/${repo}`, { permissions: { push: true, + maintain: true, }, clone_url: `https://api.github.local/${owner}/${repo}.git`, }); @@ -2091,6 +2393,7 @@ test('Throw SemanticReleaseError if "failComment" option is a whitespace String' .getOnce(`https://api.github.local/repos/${owner}/${repo}`, { permissions: { push: true, + maintain: true, }, clone_url: `https://api.github.local/${owner}/${repo}.git`, }); @@ -2131,6 +2434,7 @@ test('Throw SemanticReleaseError if "labels" option is not a String or an Array .getOnce(`https://api.github.local/repos/${owner}/${repo}`, { permissions: { push: true, + maintain: true, }, clone_url: `https://api.github.local/${owner}/${repo}.git`, }); @@ -2171,6 +2475,7 @@ test('Throw SemanticReleaseError if "labels" option is an Array with invalid ele .getOnce(`https://api.github.local/repos/${owner}/${repo}`, { permissions: { push: true, + maintain: true, }, clone_url: `https://api.github.local/${owner}/${repo}.git`, }); @@ -2211,6 +2516,7 @@ test('Throw SemanticReleaseError if "labels" option is a whitespace String', asy .getOnce(`https://api.github.local/repos/${owner}/${repo}`, { permissions: { push: true, + maintain: true, }, clone_url: `https://api.github.local/${owner}/${repo}.git`, }); @@ -2251,6 +2557,7 @@ test('Throw SemanticReleaseError if "assignees" option is not a String or an Arr .getOnce(`https://api.github.local/repos/${owner}/${repo}`, { permissions: { push: true, + maintain: true, }, clone_url: `https://api.github.local/${owner}/${repo}.git`, }); @@ -2291,6 +2598,7 @@ test('Throw SemanticReleaseError if "assignees" option is an Array with invalid .getOnce(`https://api.github.local/repos/${owner}/${repo}`, { permissions: { push: true, + maintain: true, }, clone_url: `https://api.github.local/${owner}/${repo}.git`, }); @@ -2331,6 +2639,7 @@ test('Throw SemanticReleaseError if "assignees" option is a whitespace String', .getOnce(`https://api.github.local/repos/${owner}/${repo}`, { permissions: { push: true, + maintain: true, }, clone_url: `https://api.github.local/${owner}/${repo}.git`, }); @@ -2371,6 +2680,7 @@ test('Throw SemanticReleaseError if "releasedLabels" option is not a String or a .getOnce(`https://api.github.local/repos/${owner}/${repo}`, { permissions: { push: true, + maintain: true, }, clone_url: `https://api.github.local/${owner}/${repo}.git`, }); @@ -2411,6 +2721,7 @@ test('Throw SemanticReleaseError if "releasedLabels" option is an Array with inv .getOnce(`https://api.github.local/repos/${owner}/${repo}`, { permissions: { push: true, + maintain: true, }, clone_url: `https://api.github.local/${owner}/${repo}.git`, }); @@ -2451,6 +2762,7 @@ test('Throw SemanticReleaseError if "releasedLabels" option is a whitespace Stri .getOnce(`https://api.github.local/repos/${owner}/${repo}`, { permissions: { push: true, + maintain: true, }, clone_url: `https://api.github.local/${owner}/${repo}.git`, }); @@ -2491,6 +2803,7 @@ test('Throw SemanticReleaseError if "addReleases" option is not a valid string ( .getOnce(`https://api.github.local/repos/${owner}/${repo}`, { permissions: { push: true, + maintain: true, }, clone_url: `https://api.github.local/${owner}/${repo}.git`, }); @@ -2531,6 +2844,7 @@ test('Throw SemanticReleaseError if "addReleases" option is not a valid string ( .getOnce(`https://api.github.local/repos/${owner}/${repo}`, { permissions: { push: true, + maintain: true, }, clone_url: `https://api.github.local/${owner}/${repo}.git`, }); @@ -2571,6 +2885,7 @@ test('Throw SemanticReleaseError if "addReleases" option is not a valid string ( .getOnce(`https://api.github.local/repos/${owner}/${repo}`, { permissions: { push: true, + maintain: true, }, clone_url: `https://api.github.local/${owner}/${repo}.git`, }); @@ -2611,6 +2926,7 @@ test('Throw SemanticReleaseError if "draftRelease" option is not a valid boolean .getOnce(`https://api.github.local/repos/${owner}/${repo}`, { permissions: { push: true, + maintain: true, }, clone_url: `https://api.github.local/${owner}/${repo}.git`, }); @@ -2650,6 +2966,7 @@ test('Throw SemanticReleaseError if "releaseBodyTemplate" option is an empty str .getOnce(`https://api.github.local/repos/${owner}/${repo}`, { permissions: { push: true, + maintain: true, }, clone_url: `https://api.github.local/${owner}/${repo}.git`, }); @@ -2689,6 +3006,7 @@ test('Throw SemanticReleaseError if "releaseNameTemplate" option is an empty str .getOnce(`https://api.github.local/repos/${owner}/${repo}`, { permissions: { push: true, + maintain: true, }, clone_url: `https://api.github.local/${owner}/${repo}.git`, });