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
10 changes: 8 additions & 2 deletions 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 @@ -25,11 +26,12 @@ export default async (pluginConfig, context) => {
assignee,
retryLimit,
retryStatusCodes,
useJobToken,
} = resolveConfig(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 All @@ -41,7 +43,11 @@ export default async (pluginConfig, context) => {
logger.error(`Failure reporting should be disabled via 'failCommentCondition'.
Using 'false' for 'failComment' or 'failTitle' is deprecated and will be removed in a future major version.`);
} else if (failCommentCondition === false) {
logger.log("Skip issue creation.");
logger.log(
"Skip issue creation." + useJobToken
? " Setting 'failComment' or 'failTitle' has no effect when 'useJobToken' is set."
: ""
);
} else {
const encodedFailTitle = encodeURIComponent(failTitle);
const description = failComment ? template(failComment)({ branch, errors }) : getFailComment(branch, errors);
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
10 changes: 8 additions & 2 deletions lib/success.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,17 +17,19 @@ export default async (pluginConfig, context) => {
} = context;
const {
gitlabToken,
tokenHeader,
gitlabUrl,
gitlabApiUrl,
successComment,
successCommentCondition,
proxy,
retryLimit,
retryStatusCodes,
useJobToken,
} = 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 All @@ -36,7 +38,11 @@ export default async (pluginConfig, context) => {
logger.error(`Issue and pull request comments should be disabled via 'successCommentCondition'.
Using 'false' for 'successComment' is deprecated and will be removed in a future major version.`);
} else if (successCommentCondition === false) {
logger.log("Skip commenting on issues and pull requests.");
logger.log(
"Skip commenting on issues and pull requests." + useJobToken
? " Setting 'successComment' has no effect when 'useJobToken' is set."
: ""
);
} else {
const releaseInfos = releases.filter((release) => Boolean(release.name));
try {
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
5 changes: 0 additions & 5 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 6 additions & 2 deletions test/helpers/mock-gitlab.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import urlJoin from 'url-join';
export default function (
env = {},
{
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'
Expand All @@ -22,5 +23,8 @@ export default function (
: 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 } });
}
31 changes: 31 additions & 0 deletions test/integration.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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());
});
24 changes: 24 additions & 0 deletions test/resolve-config.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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],
Expand Down Expand Up @@ -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,
}
);
});
Loading
Loading