diff --git a/README.md b/README.md index 840daf53..c28453fd 100644 --- a/README.md +++ b/README.md @@ -58,6 +58,20 @@ Create a [project access token](https://docs.gitlab.com/user/project/settings/pr **Note**: When running with [`dryRun`](https://semantic-release.gitbook.io/semantic-release/usage/configuration#dryrun) only `read_repository` scope is required. +#### Using a CI Job Token + +When running in a GitLab CI/CD environment, you can use the `CI_JOB_TOKEN` for authentication. To enable this, set the `useJobToken` option to `true` in your plugin configuration: + +```json +{ + "plugins": [ + ["@semantic-release/gitlab", { "useJobToken": true }] + ] +} +``` + +> **Important**: When `useJobToken` is enabled, comments on issues and merge requests are automatically disabled. This is due to the limited permissions of the `CI_JOB_TOKEN` which do not allow for these actions. + ### Environment variables | Variable | Description | @@ -65,6 +79,7 @@ Create a [project access token](https://docs.gitlab.com/user/project/settings/pr | `GL_TOKEN` or `GITLAB_TOKEN` | **Required.** The token used to authenticate with GitLab. | | `GL_URL` or `GITLAB_URL` | The GitLab endpoint. | | `GL_PREFIX` or `GITLAB_PREFIX` | The GitLab API prefix. | +| `CI_JOB_TOKEN` | The GitLab CI/CD job token. Used if `useJobToken` is `true`. | | `HTTP_PROXY` or `HTTPS_PROXY` | HTTP or HTTPS proxy to use. | | `NO_PROXY` | Patterns for which the proxy should be ignored. See [details below](#proxy-configuration). | @@ -86,6 +101,7 @@ If you need to bypass the proxy for some hosts, configure the `NO_PROXY` environ | ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `gitlabUrl` | The GitLab endpoint. | `GL_URL` or `GITLAB_URL` environment variable or CI provided environment variables if running on [GitLab CI/CD](https://docs.gitlab.com/ci/) or `https://gitlab.com`. | | `gitlabApiPathPrefix` | The GitLab API prefix. | `GL_PREFIX` or `GITLAB_PREFIX` environment variable or CI provided environment variables if running on [GitLab CI/CD](https://docs.gitlab.com/ci/) or `/api/v4`. | +| `useJobToken` | Set to `true` to use the `CI_JOB_TOKEN` for authentication within a GitLab CI/CD environment. | `false` | | `assets` | An array of files to upload to the release. See [assets](#assets). | - | | `milestones` | An array of milestone titles to associate to the release. See [GitLab Release API](https://docs.gitlab.com/api/releases/#create-a-release). | - | | `successComment` | The comment to add to each Issue and Merge Request resolved by the release. See [successComment](#successComment). | :tada: This issue has been resolved in version ${nextRelease.version} :tada:\n\nThe release is available on [GitLab release](gitlab_release_url) | diff --git a/lib/definitions/constants.js b/lib/definitions/constants.js index 18f1ecd3..7f04c590 100644 --- a/lib/definitions/constants.js +++ b/lib/definitions/constants.js @@ -1,3 +1,3 @@ -export const HOME_URL = 'https://github.com/semantic-release/semantic-release'; +export const HOME_URL = "https://github.com/semantic-release/semantic-release"; -export const RELEASE_NAME = 'GitLab release'; +export const RELEASE_NAME = "GitLab release"; diff --git a/lib/definitions/errors.js b/lib/definitions/errors.js index 51508538..e8a55cc3 100644 --- a/lib/definitions/errors.js +++ b/lib/definitions/errors.js @@ -1,88 +1,88 @@ -import {inspect} from 'util'; +import { inspect } from "util"; import { createRequire } from "node:module"; const require = createRequire(import.meta.url); const pkg = require("../../package.json"); -const [homepage] = pkg.homepage.split('#'); +const [homepage] = pkg.homepage.split("#"); const linkify = (file) => `${homepage}/blob/master/${file}`; -const stringify = (object) => inspect(object, {breakLength: Number.POSITIVE_INFINITY, depth: 2, maxArrayLength: 5}); +const stringify = (object) => inspect(object, { breakLength: Number.POSITIVE_INFINITY, depth: 2, maxArrayLength: 5 }); export default { - EINVALIDASSETS: ({assets}) => ({ - message: 'Invalid `assets` option.', + EINVALIDASSETS: ({ assets }) => ({ + message: "Invalid `assets` option.", details: `The [assets option](${linkify( - 'README.md#assets' + "README.md#assets" )}) must be an \`Array\` of \`Strings\` or \`Objects\` with a \`path\` property. Your configuration for the \`assets\` option is \`${stringify(assets)}\`.`, }), - EINVALIDFAILTITLE: ({failTitle}) => ({ - message: 'Invalid `failTitle` option.', - details: `The [failTitle option](${linkify('README.md#failtitle')}) if defined, must be a non empty \`String\`. + EINVALIDFAILTITLE: ({ failTitle }) => ({ + message: "Invalid `failTitle` option.", + details: `The [failTitle option](${linkify("README.md#failtitle")}) if defined, must be a non empty \`String\`. Your configuration for the \`failTitle\` option is \`${stringify(failTitle)}\`.`, }), - EINVALIDFAILCOMMENT: ({failComment}) => ({ - message: 'Invalid `failComment` option.', - details: `The [failComment option](${linkify('README.md#failcomment')}) if defined, must be a non empty \`String\`. + EINVALIDFAILCOMMENT: ({ failComment }) => ({ + message: "Invalid `failComment` option.", + details: `The [failComment option](${linkify("README.md#failcomment")}) if defined, must be a non empty \`String\`. Your configuration for the \`failComment\` option is \`${stringify(failComment)}\`.`, }), - EINVALIDLABELS: ({labels}) => ({ - message: 'Invalid `labels` option.', - details: `The [labels option](${linkify('README.md#labels')}) if defined, must be a non empty \`String\`. + EINVALIDLABELS: ({ labels }) => ({ + message: "Invalid `labels` option.", + details: `The [labels option](${linkify("README.md#labels")}) if defined, must be a non empty \`String\`. Your configuration for the \`labels\` option is \`${stringify(labels)}\`.`, }), - EINVALIDASSIGNEE: ({assignee}) => ({ - message: 'Invalid `assignee` option.', - details: `The [assignee option](${linkify('README.md#assignee')}) if defined, must be a non empty \`String\`. + EINVALIDASSIGNEE: ({ assignee }) => ({ + message: "Invalid `assignee` option.", + details: `The [assignee option](${linkify("README.md#assignee")}) if defined, must be a non empty \`String\`. Your configuration for the \`assignee\` option is \`${stringify(assignee)}\`.`, }), EINVALIDGITLABURL: () => ({ - message: 'The git repository URL is not a valid GitLab URL.', + message: "The git repository URL is not a valid GitLab URL.", details: `The **semantic-release** \`repositoryUrl\` option must a valid GitLab URL with the format \`/.git\`. 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.`, }), - EINVALIDGLTOKEN: ({projectPath}) => ({ - message: 'Invalid GitLab token.', + EINVALIDGLTOKEN: ({ projectPath }) => ({ + message: "Invalid GitLab token.", details: `The [GitLab token](${linkify( - 'README.md#gitlab-authentication' + "README.md#gitlab-authentication" )}) configured in the \`GL_TOKEN\` or \`GITLAB_TOKEN\` environment variable must be a valid [personal access token](https://docs.gitlab.com/ce/user/profile/personal_access_tokens.html) allowing to push to the repository ${projectPath}. Please make sure to set the \`GL_TOKEN\` or \`GITLAB_TOKEN\` environment variable in your CI with the exact value of the GitLab personal token.`, }), - EMISSINGREPO: ({projectPath}) => ({ + EMISSINGREPO: ({ projectPath }) => ({ message: `The repository ${projectPath} doesn't exist.`, details: `The **semantic-release** \`repositoryUrl\` option must refer to your GitLab repository. The repository must be accessible with the [GitLab API](https://docs.gitlab.com/ce/api/README.html). 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. If you are using [GitLab Enterprise Edition](https://about.gitlab.com/gitlab-ee) please make sure to configure the \`gitlabUrl\` and \`gitlabApiPathPrefix\` [options](${linkify( - 'README.md#options' + "README.md#options" )}).`, }), - EGLNOPUSHPERMISSION: ({projectPath}) => ({ + EGLNOPUSHPERMISSION: ({ projectPath }) => ({ message: `The GitLab token doesn't allow to push on the repository ${projectPath}.`, details: `The user associated with the [GitLab token](${linkify( - 'README.md#gitlab-authentication' + "README.md#gitlab-authentication" )}) configured in the \`GL_TOKEN\` or \`GITLAB_TOKEN\` environment variable must allows to push to the repository ${projectPath}. Please make sure the GitLab user associated with the token has the [permission to push](https://docs.gitlab.com/ee/user/permissions.html#project-members-permissions) to the repository ${projectPath}.`, }), - EGLNOPULLPERMISSION: ({projectPath}) => ({ + EGLNOPULLPERMISSION: ({ projectPath }) => ({ message: `The GitLab token doesn't allow to pull from the repository ${projectPath}.`, details: `The user associated with the [GitLab token](${linkify( - 'README.md#gitlab-authentication' + "README.md#gitlab-authentication" )}) configured in the \`GL_TOKEN\` or \`GITLAB_TOKEN\` environment variable must allow pull from the repository ${projectPath}. Please make sure the GitLab user associated with the token has the [permission to push](https://docs.gitlab.com/ee/user/permissions.html#project-members-permissions) to the repository ${projectPath}.`, }), - ENOGLTOKEN: ({repositoryUrl}) => ({ - message: 'No GitLab token specified.', + ENOGLTOKEN: ({ repositoryUrl }) => ({ + message: "No GitLab token specified.", details: `A [GitLab personal access token](${linkify( - 'README.md#gitlab-authentication' + "README.md#gitlab-authentication" )}) must be created and set in the \`GL_TOKEN\` or \`GITLAB_TOKEN\` environment variable on your CI environment. Please make sure to create a [GitLab personal access token](https://docs.gitlab.com/ce/user/profile/personal_access_tokens.html) and to set it in the \`GL_TOKEN\` or \`GITLAB_TOKEN\` environment variable on your CI environment. The token must allow to push to the repository ${repositoryUrl}.`, diff --git a/lib/fail.js b/lib/fail.js index 5f878c4d..ce7221b6 100644 --- a/lib/fail.js +++ b/lib/fail.js @@ -16,6 +16,7 @@ export default async (pluginConfig, context) => { } = context; const { gitlabToken, + tokenHeader, gitlabUrl, gitlabApiUrl, failComment, @@ -29,7 +30,7 @@ export default async (pluginConfig, context) => { const { encodedProjectPath, projectApiUrl } = getProjectContext(context, gitlabUrl, gitlabApiUrl, repositoryUrl); const apiOptions = { - headers: { "PRIVATE-TOKEN": gitlabToken }, + headers: { [tokenHeader]: gitlabToken }, retry: { limit: retryLimit, statusCodes: retryStatusCodes, diff --git a/lib/publish.js b/lib/publish.js index 39497268..a86c4ac9 100644 --- a/lib/publish.js +++ b/lib/publish.js @@ -22,7 +22,7 @@ export default async (pluginConfig, context) => { nextRelease: { gitTag, gitHead, notes, version }, logger, } = context; - const { gitlabToken, gitlabUrl, gitlabApiUrl, assets, milestones, proxy, retryLimit, retryStatusCodes } = + const { gitlabToken, tokenHeader, gitlabUrl, gitlabApiUrl, assets, milestones, proxy, retryLimit, retryStatusCodes } = resolveConfig(pluginConfig, context); const assetsList = []; const { projectPath, projectApiUrl } = getProjectContext(context, gitlabUrl, gitlabApiUrl, repositoryUrl); @@ -30,7 +30,7 @@ export default async (pluginConfig, context) => { const encodedGitTag = encodeURIComponent(gitTag); const apiOptions = { headers: { - "PRIVATE-TOKEN": gitlabToken, + [tokenHeader]: gitlabToken, }, hooks: { beforeError: [ diff --git a/lib/resolve-config.js b/lib/resolve-config.js index d26dbf37..3bb87627 100644 --- a/lib/resolve-config.js +++ b/lib/resolve-config.js @@ -16,6 +16,7 @@ export default ( labels, assignee, retryLimit, + useJobToken, }, { envCi: { service } = {}, @@ -23,6 +24,7 @@ export default ( CI_PROJECT_URL, CI_PROJECT_PATH, CI_API_V4_URL, + CI_JOB_TOKEN, GL_TOKEN, GITLAB_TOKEN, GL_URL, @@ -51,7 +53,9 @@ export default ( ? CI_PROJECT_URL.replace(new RegExp(`/${CI_PROJECT_PATH}$`), "") : "https://gitlab.com"); return { - gitlabToken: GL_TOKEN || GITLAB_TOKEN, + gitlabToken: useJobToken ? CI_JOB_TOKEN : GL_TOKEN || GITLAB_TOKEN, + tokenHeader: useJobToken ? "JOB-TOKEN" : "PRIVATE-TOKEN", + useJobToken, gitlabUrl: defaultedGitlabUrl, gitlabApiUrl: userGitlabUrl && userGitlabApiPathPrefix @@ -62,11 +66,11 @@ export default ( assets: assets ? castArray(assets) : assets, milestones: milestones ? castArray(milestones) : milestones, successComment, - successCommentCondition, + successCommentCondition: useJobToken ? false : successCommentCondition, proxy: getProxyConfiguration(defaultedGitlabUrl, HTTP_PROXY, HTTPS_PROXY, NO_PROXY), failTitle: isNil(failTitle) ? "The automated release is failing 🚨" : failTitle, failComment, - failCommentCondition, + failCommentCondition: useJobToken ? false : failCommentCondition, labels: isNil(labels) ? "semantic-release" : labels === false ? false : labels, assignee, retryLimit: retryLimit ?? DEFAULT_RETRY_LIMIT, diff --git a/lib/success.js b/lib/success.js index b2ec1180..7a004329 100644 --- a/lib/success.js +++ b/lib/success.js @@ -17,6 +17,7 @@ export default async (pluginConfig, context) => { } = context; const { gitlabToken, + tokenHeader, gitlabUrl, gitlabApiUrl, successComment, @@ -27,7 +28,7 @@ export default async (pluginConfig, context) => { } = resolveConfig(pluginConfig, context); const { projectApiUrl } = getProjectContext(context, gitlabUrl, gitlabApiUrl, repositoryUrl); const apiOptions = { - headers: { "PRIVATE-TOKEN": gitlabToken }, + headers: { [tokenHeader]: gitlabToken }, retry: { limit: retryLimit, statusCodes: retryStatusCodes }, }; diff --git a/lib/verify.js b/lib/verify.js index fdfc3b7f..bade808f 100644 --- a/lib/verify.js +++ b/lib/verify.js @@ -6,6 +6,7 @@ import AggregateError from "aggregate-error"; import resolveConfig from "./resolve-config.js"; import getProjectContext from "./get-project-context.js"; import getError from "./get-error.js"; +import urlJoin from "url-join"; const isNonEmptyString = (value) => isString(value) && value.trim(); const isStringOrStringArray = (value) => @@ -30,7 +31,10 @@ export default async (pluginConfig, context) => { options: { repositoryUrl }, logger, } = context; - const { gitlabToken, gitlabUrl, gitlabApiUrl, proxy, ...options } = resolveConfig(pluginConfig, context); + const { gitlabToken, gitlabUrl, gitlabApiUrl, tokenHeader, useJobToken, proxy, ...options } = resolveConfig( + pluginConfig, + context + ); const { projectPath, projectApiUrl } = getProjectContext(context, gitlabUrl, gitlabApiUrl, repositoryUrl); debug("apiUrl: %o", gitlabApiUrl); @@ -60,23 +64,28 @@ export default async (pluginConfig, context) => { logger.log("Verify GitLab authentication (%s)", gitlabApiUrl); try { - ({ - permissions: { project_access: projectAccess, group_access: groupAccess }, - } = await got - .get(projectApiUrl, { - headers: { "PRIVATE-TOKEN": gitlabToken }, - ...proxy, - }) - .json()); - if ( - context.options.dryRun && - !((projectAccess && projectAccess.access_level >= 10) || (groupAccess && groupAccess.access_level >= 10)) - ) { - errors.push(getError("EGLNOPULLPERMISSION", { projectPath })); - } else if ( - !((projectAccess && projectAccess.access_level >= 30) || (groupAccess && groupAccess.access_level >= 30)) - ) { - errors.push(getError("EGLNOPUSHPERMISSION", { projectPath })); + if (useJobToken) { + logger.log("Using Job Token for authentication. Some functionality may be disabled."); + await got.get(urlJoin(projectApiUrl, "releases"), { headers: { [tokenHeader]: gitlabToken } }); + } else { + ({ + permissions: { project_access: projectAccess, group_access: groupAccess }, + } = await got + .get(projectApiUrl, { + headers: { [tokenHeader]: gitlabToken }, + ...proxy, + }) + .json()); + if ( + context.options.dryRun && + !((projectAccess && projectAccess.access_level >= 10) || (groupAccess && groupAccess.access_level >= 10)) + ) { + errors.push(getError("EGLNOPULLPERMISSION", { projectPath })); + } else if ( + !((projectAccess && projectAccess.access_level >= 30) || (groupAccess && groupAccess.access_level >= 30)) + ) { + errors.push(getError("EGLNOPUSHPERMISSION", { projectPath })); + } } } catch (error) { if (error.response && error.response.statusCode === 401) { diff --git a/package-lock.json b/package-lock.json index 05e8843e..9e0ae39d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -325,7 +325,6 @@ "integrity": "sha512-t54CUOsFMappY1Jbzb7fetWeO0n6K0k/4+/ZpkS+3Joz8I4VcvY9OiEBFRYISqaI2fq5sCiPtAjRDOzVYG8m+Q==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@octokit/auth-token": "^6.0.0", "@octokit/graphql": "^9.0.2", @@ -954,7 +953,6 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -3688,7 +3686,6 @@ "integrity": "sha512-8dD6FusOQSrpv9Z1rdNMdlSgQOIP880DHqnohobOmYLElGEqAL/JvxvuxZO16r4HtjTlfPRDC1hbvxC9dPN2nA==", "dev": true, "license": "MIT", - "peer": true, "bin": { "marked": "bin/marked.js" }, @@ -6502,7 +6499,6 @@ "dev": true, "inBundle": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -7598,7 +7594,6 @@ "integrity": "sha512-0OCYLm0AfVilNGukM+w0C4aptITfuW1Mhvmz8LQliLeYbPOTFRCIJzoltWWx/F5zVFe6np9eNatBUHdAvMFeZg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@semantic-release/commit-analyzer": "^13.0.1", "@semantic-release/error": "^4.0.0", diff --git a/test/helpers/mock-gitlab.js b/test/helpers/mock-gitlab.js index d9c2eb7f..dcc7587b 100644 --- a/test/helpers/mock-gitlab.js +++ b/test/helpers/mock-gitlab.js @@ -1,26 +1,32 @@ -import nock from 'nock'; -import urlJoin from 'url-join'; +import nock from "nock"; +import urlJoin from "url-join"; /** - * Retun a `nock` object setup to respond to a GitLab authentication request. Other expectation and responses can be chained. + * Return a `nock` object setup to respond to a GitLab authentication request. Other expectation and responses can be chained. * * @param {Object} [env={}] Environment variables. - * @param {String} [gitlabToken=env.GL_TOKEN || env.GITLAB_TOKEN || 'GL_TOKEN'] The github token to return in the authentication response. - * @param {String} [gitlabUrl=env.GL_URL || env.GITLAB_URL || 'https://api.github.com'] The url on which to intercept http requests. - * @param {String} [gitlabApiPathPrefix=env.GL_PREFIX || env.GITLAB_PREFIX || ''] The GitHub Enterprise API prefix. - * @return {Object} A `nock` object ready to respond to a github authentication request. + * @param {Object} [options={}] Options. + * @param {boolean} [options.useJobToken=false] Whether to use a CI_JOB_TOKEN. + * @param {String} [options.gitlabToken=env.GL_TOKEN || env.GITLAB_TOKEN || 'GL_TOKEN'] The GitLab token to use for authentication. + * @param {String} [options.gitlabUrl=env.GL_URL || env.GITLAB_URL || 'https://gitlab.com'] The url on which to intercept http requests. + * @param {String} [options.gitlabApiPathPrefix=env.GL_PREFIX || env.GITLAB_PREFIX || '/api/v4'] The GitLab API prefix. + * @return {Object} A `nock` object ready to respond to a GitLab authentication request. */ export default function ( env = {}, { - gitlabToken = env.GL_TOKEN || env.GITLAB_TOKEN || 'GL_TOKEN', - gitlabUrl = env.GL_URL || env.GITLAB_URL || 'https://gitlab.com', - gitlabApiPathPrefix = typeof env.GL_PREFIX === 'string' + useJobToken = false, + gitlabToken = env.GL_TOKEN || env.GITLAB_TOKEN || "GL_TOKEN", + gitlabUrl = env.GL_URL || env.GITLAB_URL || "https://gitlab.com", + gitlabApiPathPrefix = typeof env.GL_PREFIX === "string" ? env.GL_PREFIX - : null || typeof env.GITLAB_PREFIX === 'string' - ? env.GITLAB_PREFIX - : null || '/api/v4', + : null || typeof env.GITLAB_PREFIX === "string" + ? env.GITLAB_PREFIX + : null || "/api/v4", } = {} ) { - return nock(urlJoin(gitlabUrl, gitlabApiPathPrefix), {reqheaders: {'Private-Token': gitlabToken}}); -}; + const tokenHeader = useJobToken ? "JOB-TOKEN" : "Private-Token"; + const token = useJobToken ? env.CI_JOB_TOKEN : gitlabToken; + + return nock(urlJoin(gitlabUrl, gitlabApiPathPrefix), { reqheaders: { [tokenHeader]: token } }); +} diff --git a/test/integration.test.js b/test/integration.test.js index dbfd9f2c..4b8863f3 100644 --- a/test/integration.test.js +++ b/test/integration.test.js @@ -112,3 +112,34 @@ test.serial("Verify Github auth and release", async (t) => { t.deepEqual(t.context.log.args[1], ["Published GitLab release: %s", nextRelease.gitTag]); t.true(gitlab.isDone()); }); + +test.serial("Verify GitLab auth and release with Job Token", async (t) => { + const env = { CI_JOB_TOKEN: "job_token" }; + const owner = "test_user"; + const repo = "test_repo"; + const options = { repositoryUrl: `https://gitlab.com/${owner}/${repo}.git` }; + const encodedProjectPath = encodeURIComponent(`${owner}/${repo}`); + const nextRelease = { gitHead: "123", gitTag: "v1.0.0", notes: "Test release note body" }; + const pluginConfig = { useJobToken: true }; + + const gitlab = authenticate(env, { useJobToken: true }) + .get(`/projects/${encodedProjectPath}/releases`) + .reply(200) + .post(`/projects/${encodedProjectPath}/releases`, { + tag_name: nextRelease.gitTag, + description: nextRelease.notes, + assets: { + links: [], + }, + }) + .reply(200, {}); + + await t.notThrowsAsync(t.context.m.verifyConditions(pluginConfig, { env, options, logger: t.context.logger })); + const result = await t.context.m.publish(pluginConfig, { env, options, nextRelease, logger: t.context.logger }); + + t.is(result.url, `https://gitlab.com/${owner}/${repo}/-/releases/${nextRelease.gitTag}`); + t.deepEqual(t.context.log.args[0], ["Verify GitLab authentication (%s)", "https://gitlab.com/api/v4"]); + t.deepEqual(t.context.log.args[1], ["Using Job Token for authentication. Some functionality may be disabled."]); + t.deepEqual(t.context.log.args[2], ["Published GitLab release: %s", nextRelease.gitTag]); + t.true(gitlab.isDone()); +}); diff --git a/test/resolve-config.test.js b/test/resolve-config.test.js index 5f800e78..931cf677 100644 --- a/test/resolve-config.test.js +++ b/test/resolve-config.test.js @@ -16,6 +16,8 @@ const defaultOptions = { failCommentCondition: undefined, labels: "semantic-release", assignee: undefined, + tokenHeader: "PRIVATE-TOKEN", + useJobToken: undefined, proxy: {}, retryLimit: 3, retryStatusCodes: [408, 413, 422, 429, 500, 502, 503, 504, 521, 522, 524], @@ -508,3 +510,25 @@ test("Ignore GitLab CI/CD environment variables if not running on GitLab CI/CD", } ); }); + +test("Set token header to JOB-TOKEN when useJobToken is set to true", (t) => { + const jobToken = "TOKEN"; + + t.deepEqual( + resolveConfig( + { useJobToken: true }, + { + envCi: { service: "gitlab" }, + env: { CI_JOB_TOKEN: jobToken }, + } + ), + { + ...defaultOptions, + gitlabToken: jobToken, + useJobToken: true, + tokenHeader: "JOB-TOKEN", + successCommentCondition: false, + failCommentCondition: false, + } + ); +}); diff --git a/test/verify.test.js b/test/verify.test.js index 48f3004f..b0b1f3b9 100644 --- a/test/verify.test.js +++ b/test/verify.test.js @@ -86,6 +86,44 @@ test.serial("Verify token and repository access (group_access 40)", async (t) => t.true(gitlab.isDone()); }); +test.serial("Verify CI_JOB_TOKEN and repository access", async (t) => { + const owner = "test_user"; + const repo = "test_repo"; + const env = { CI_JOB_TOKEN: "job_token" }; + const gitlab = authenticate(env, { useJobToken: true }).get(`/projects/${owner}%2F${repo}/releases`).reply(200); + + await t.notThrowsAsync( + verify( + { useJobToken: true }, + { env, options: { repositoryUrl: `git+https://gitlab.com/${owner}/${repo}.git` }, logger: t.context.logger } + ) + ); + + t.true(gitlab.isDone()); + t.deepEqual(t.context.log.args[1], ["Using Job Token for authentication. Some functionality may be disabled."]); +}); + +test.serial("Throw SemanticReleaseError for invalid CI_JOB_TOKEN", async (t) => { + const owner = "test_user"; + const repo = "test_repo"; + const env = { CI_JOB_TOKEN: "job_token" }; + const gitlab = authenticate(env, { useJobToken: true }).get(`/projects/${owner}%2F${repo}/releases`).reply(401); + + const { + errors: [error, ...errors], + } = await t.throwsAsync( + verify( + { useJobToken: true }, + { env, options: { repositoryUrl: `https://gitlab.com:${owner}/${repo}.git` }, logger: t.context.logger } + ) + ); + + t.is(errors.length, 0); + t.is(error.name, "SemanticReleaseError"); + t.is(error.code, "EINVALIDGLTOKEN"); + t.true(gitlab.isDone()); +}); + test.serial("Verify token and repository access and custom URL with prefix", async (t) => { const owner = "test_user"; const repo = "test_repo";