Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,13 +58,28 @@ 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 |
| ------------------------------ | ------------------------------------------------------------------------------------------ |
| `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). |

Expand All @@ -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) |
Expand Down
4 changes: 2 additions & 2 deletions lib/definitions/constants.js
Original file line number Diff line number Diff line change
@@ -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";
62 changes: 31 additions & 31 deletions lib/definitions/errors.js
Original file line number Diff line number Diff line change
@@ -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 \`<GitLab_URL>/<projectPath>.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}.`,
Expand Down
3 changes: 2 additions & 1 deletion lib/fail.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export default async (pluginConfig, context) => {
} = context;
const {
gitlabToken,
tokenHeader,
gitlabUrl,
gitlabApiUrl,
failComment,
Expand All @@ -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,
Expand Down
4 changes: 2 additions & 2 deletions lib/publish.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,15 +22,15 @@ 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);

const encodedGitTag = encodeURIComponent(gitTag);
const apiOptions = {
headers: {
"PRIVATE-TOKEN": gitlabToken,
[tokenHeader]: gitlabToken,
},
hooks: {
beforeError: [
Expand Down
10 changes: 7 additions & 3 deletions lib/resolve-config.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,15 @@ export default (
labels,
assignee,
retryLimit,
useJobToken,
},
{
envCi: { service } = {},
env: {
CI_PROJECT_URL,
CI_PROJECT_PATH,
CI_API_V4_URL,
CI_JOB_TOKEN,
GL_TOKEN,
GITLAB_TOKEN,
GL_URL,
Expand Down Expand Up @@ -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
Expand All @@ -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,
Expand Down
3 changes: 2 additions & 1 deletion lib/success.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export default async (pluginConfig, context) => {
} = context;
const {
gitlabToken,
tokenHeader,
gitlabUrl,
gitlabApiUrl,
successComment,
Expand All @@ -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 },
};

Expand Down
45 changes: 27 additions & 18 deletions lib/verify.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) =>
Expand All @@ -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);
Expand Down Expand Up @@ -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) {
Expand Down
Loading