Skip to content

Commit 542a2eb

Browse files
feat: enable support for using job token authentication
1 parent 304b857 commit 542a2eb

File tree

11 files changed

+158
-30
lines changed

11 files changed

+158
-30
lines changed

README.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,10 +54,19 @@ With this example [GitLab releases](https://docs.gitlab.com/ee/user/project/rele
5454
The GitLab authentication configuration is **required** and can be set via
5555
[environment variables](#environment-variables).
5656

57+
There are two types of tokens supported by GitLab:
58+
59+
#### Access Token
60+
5761
Create a [project access token](https://docs.gitlab.com/ee/user/project/settings/project_access_tokens.html), [group access token](https://docs.gitlab.com/ee/user/group/settings/group_access_tokens.html), or [personal access token](https://docs.gitlab.com/ce/user/profile/personal_access_tokens.html) with role _Developer_ (or higher) and the `api` scope and make it available in your CI environment via the `GL_TOKEN` environment variable. If you are using `GL_TOKEN` as the [remote Git repository authentication](https://github.com/semantic-release/semantic-release/blob/master/docs/usage/ci-configuration.md#authentication) it must also have the `write_repository` scope.
5862

5963
**Note**: When running with [`dryRun`](https://semantic-release.gitbook.io/semantic-release/usage/configuration#dryrun) only `read_repository` scope is required.
6064

65+
#### Job Token
66+
Ensure your project is configured to [allow git push requests for job tokens](https://docs.gitlab.com/ci/jobs/ci_job_token/#allow-git-push-requests-to-your-project-repository), and assign the value of `CI_JOB_TOKEN` to `GL_TOKEN`.
67+
68+
**Note**: Due to limitations on [job token](https://docs.gitlab.com/ci/jobs/ci_job_token/) access, comments on merge requests and issues must be explicitly disabled. See: [successCommentCondition](#successcommentcondition) and [failCommentCondition](#failcommentcondition).
69+
6170
### Environment variables
6271

6372
| Variable | Description |

lib/definitions/errors.js

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -49,13 +49,13 @@ By default the \`repositoryUrl\` option is retrieved from the \`repository\` pro
4949
message: 'Invalid GitLab token.',
5050
details: `The [GitLab token](${linkify(
5151
'README.md#gitlab-authentication'
52-
)}) 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}.
52+
)}) configured in the \`GL_TOKEN\` or \`GITLAB_TOKEN\` environment variable must be a valid [ci job token](https://docs.gitlab.com/ci/jobs/ci_job_token/), [group access token](https://docs.gitlab.com/user/group/settings/group_access_tokens/), [project access token](https://docs.gitlab.com/user/project/settings/project_access_tokens/), or [personal access token](https://docs.gitlab.com/user/profile/personal_access_tokens/) with access to the repository ${projectPath}.
5353
54-
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.`,
54+
Please make sure to set the \`GL_TOKEN\` or \`GITLAB_TOKEN\` environment variable in your CI with the exact value of the GitLab token.`,
5555
}),
5656
EMISSINGREPO: ({projectPath}) => ({
5757
message: `The repository ${projectPath} doesn't exist.`,
58-
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).
58+
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/api/rest/).
5959
6060
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.
6161
@@ -65,15 +65,15 @@ If you are using [GitLab Enterprise Edition](https://about.gitlab.com/gitlab-ee)
6565
}),
6666
EGLNOPUSHPERMISSION: ({projectPath}) => ({
6767
message: `The GitLab token doesn't allow to push on the repository ${projectPath}.`,
68-
details: `The user associated with the [GitLab token](${linkify(
68+
details: `The access associated with the [GitLab token](${linkify(
6969
'README.md#gitlab-authentication'
70-
)}) configured in the \`GL_TOKEN\` or \`GITLAB_TOKEN\` environment variable must allows to push to the repository ${projectPath}.
70+
)}) configured in the \`GL_TOKEN\` or \`GITLAB_TOKEN\` environment variable must allow push to the repository ${projectPath}.
7171
7272
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}.`,
7373
}),
7474
EGLNOPULLPERMISSION: ({projectPath}) => ({
7575
message: `The GitLab token doesn't allow to pull from the repository ${projectPath}.`,
76-
details: `The user associated with the [GitLab token](${linkify(
76+
details: `The access associated with the [GitLab token](${linkify(
7777
'README.md#gitlab-authentication'
7878
)}) configured in the \`GL_TOKEN\` or \`GITLAB_TOKEN\` environment variable must allow pull from the repository ${projectPath}.
7979
@@ -85,6 +85,13 @@ Please make sure the GitLab user associated with the token has the [permission t
8585
'README.md#gitlab-authentication'
8686
)}) must be created and set in the \`GL_TOKEN\` or \`GITLAB_TOKEN\` environment variable on your CI environment.
8787
88-
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}.`,
88+
Please make sure to create a [group access token](https://docs.gitlab.com/user/group/settings/group_access_tokens/), [project access token](https://docs.gitlab.com/user/project/settings/project_access_tokens/), [personal access token](https://docs.gitlab.com/user/profile/personal_access_tokens/), or utilize the [ci job token](https://docs.gitlab.com/ci/jobs/ci_job_token/) and to set it in the \`GL_TOKEN\` or \`GITLAB_TOKEN\` environment variable on your CI environment. The token must allow access to the repository ${repositoryUrl}.`,
89+
}),
90+
91+
EJOBTOKENCOMMENTCONDITION: ({projectPath}) => ({
92+
message: 'Invalid comment conditions using job token.',
93+
details: `When using a [job token](https://docs.gitlab.com/ci/jobs/ci_job_token/), [successCommentCondition](${linkify('README.md#successCommentCondition')}) and [failCommentCondition](${linkify('README.md#failCommentCondition')}) must be explicitly set to \`false\`, as job tokens do not have permissions to comment on issues and merge requests.
94+
95+
Please explicitly disable this function, or use a [group access token](https://docs.gitlab.com/user/group/settings/group_access_tokens/), [project access token](https://docs.gitlab.com/user/project/settings/project_access_tokens/), or [personal access token](https://docs.gitlab.com/user/profile/personal_access_tokens/) with access to the repository ${projectPath}`,
8996
}),
9097
};

lib/fail.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ export default async (pluginConfig, context) => {
1616
} = context;
1717
const {
1818
gitlabToken,
19+
tokenHeader,
1920
gitlabUrl,
2021
gitlabApiUrl,
2122
failComment,
@@ -29,7 +30,7 @@ export default async (pluginConfig, context) => {
2930
const { encodedProjectPath, projectApiUrl } = getProjectContext(context, gitlabUrl, gitlabApiUrl, repositoryUrl);
3031

3132
const apiOptions = {
32-
headers: { "PRIVATE-TOKEN": gitlabToken },
33+
headers: { [tokenHeader]: gitlabToken },
3334
retry: {
3435
limit: retryLimit,
3536
statusCodes: retryStatusCodes,

lib/publish.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ export default async (pluginConfig, context) => {
2222
nextRelease: { gitTag, gitHead, notes, version },
2323
logger,
2424
} = context;
25-
const { gitlabToken, gitlabUrl, gitlabApiUrl, assets, milestones, proxy, retryLimit, retryStatusCodes } =
25+
const { gitlabToken, tokenHeader, gitlabUrl, gitlabApiUrl, assets, milestones, proxy, retryLimit, retryStatusCodes } =
2626
resolveConfig(pluginConfig, context);
2727
const assetsList = [];
2828
const { projectPath, projectApiUrl } = getProjectContext(context, gitlabUrl, gitlabApiUrl, repositoryUrl);
@@ -31,7 +31,7 @@ export default async (pluginConfig, context) => {
3131
const encodedVersion = encodeURIComponent(version);
3232
const apiOptions = {
3333
headers: {
34-
"PRIVATE-TOKEN": gitlabToken,
34+
[tokenHeader]: gitlabToken,
3535
},
3636
hooks: {
3737
beforeError: [

lib/resolve-config.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ export default (
2323
CI_PROJECT_URL,
2424
CI_PROJECT_PATH,
2525
CI_API_V4_URL,
26+
CI_JOB_TOKEN,
2627
GL_TOKEN,
2728
GITLAB_TOKEN,
2829
GL_URL,
@@ -52,6 +53,8 @@ export default (
5253
: "https://gitlab.com");
5354
return {
5455
gitlabToken: GL_TOKEN || GITLAB_TOKEN,
56+
isJobToken: (!!CI_JOB_TOKEN && (GL_TOKEN || GITLAB_TOKEN) === CI_JOB_TOKEN),
57+
tokenHeader: (!!CI_JOB_TOKEN && (GL_TOKEN || GITLAB_TOKEN) === CI_JOB_TOKEN) ? "JOB-TOKEN" : "PRIVATE-TOKEN",
5558
gitlabUrl: defaultedGitlabUrl,
5659
gitlabApiUrl:
5760
userGitlabUrl && userGitlabApiPathPrefix

lib/success.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ export default async (pluginConfig, context) => {
1717
} = context;
1818
const {
1919
gitlabToken,
20+
tokenHeader,
2021
gitlabUrl,
2122
gitlabApiUrl,
2223
successComment,
@@ -27,7 +28,7 @@ export default async (pluginConfig, context) => {
2728
} = resolveConfig(pluginConfig, context);
2829
const { projectApiUrl } = getProjectContext(context, gitlabUrl, gitlabApiUrl, repositoryUrl);
2930
const apiOptions = {
30-
headers: { "PRIVATE-TOKEN": gitlabToken },
31+
headers: { [tokenHeader]: gitlabToken },
3132
retry: { limit: retryLimit, statusCodes: retryStatusCodes },
3233
};
3334

lib/verify.js

Lines changed: 30 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import AggregateError from "aggregate-error";
66
import resolveConfig from "./resolve-config.js";
77
import getProjectContext from "./get-project-context.js";
88
import getError from "./get-error.js";
9+
import urlJoin from "url-join";
910

1011
const isNonEmptyString = (value) => isString(value) && value.trim();
1112
const isStringOrStringArray = (value) =>
@@ -30,7 +31,10 @@ export default async (pluginConfig, context) => {
3031
options: { repositoryUrl },
3132
logger,
3233
} = context;
33-
const { gitlabToken, gitlabUrl, gitlabApiUrl, proxy, ...options } = resolveConfig(pluginConfig, context);
34+
const { gitlabToken, isJobToken, tokenHeader, successCommentCondition, failCommentCondition, gitlabUrl, gitlabApiUrl, proxy, ...options } = resolveConfig(
35+
pluginConfig,
36+
context
37+
);
3438
const { projectPath, projectApiUrl } = getProjectContext(context, gitlabUrl, gitlabApiUrl, repositoryUrl);
3539

3640
debug("apiUrl: %o", gitlabApiUrl);
@@ -53,30 +57,38 @@ export default async (pluginConfig, context) => {
5357
errors.push(getError("ENOGLTOKEN", { repositoryUrl }));
5458
}
5559

60+
if (isJobToken && !(failCommentCondition === false) && !(successCommentCondition === false)) {
61+
errors.push(getError("EJOBTOKENCOMMENTCONDITION", { projectPath }))
62+
}
63+
5664
if (gitlabToken && projectPath) {
5765
let projectAccess;
5866
let groupAccess;
5967

6068
logger.log("Verify GitLab authentication (%s)", gitlabApiUrl);
6169

6270
try {
63-
({
64-
permissions: { project_access: projectAccess, group_access: groupAccess },
65-
} = await got
66-
.get(projectApiUrl, {
67-
headers: { "PRIVATE-TOKEN": gitlabToken },
68-
...proxy,
69-
})
70-
.json());
71-
if (
72-
context.options.dryRun &&
73-
!((projectAccess && projectAccess.access_level >= 10) || (groupAccess && groupAccess.access_level >= 10))
74-
) {
75-
errors.push(getError("EGLNOPULLPERMISSION", { projectPath }));
76-
} else if (
77-
!((projectAccess && projectAccess.access_level >= 30) || (groupAccess && groupAccess.access_level >= 30))
78-
) {
79-
errors.push(getError("EGLNOPUSHPERMISSION", { projectPath }));
71+
if (isJobToken) {
72+
await got.get(urlJoin(projectApiUrl, "releases"), { headers: { [tokenHeader]: gitlabToken } });
73+
} else {
74+
({
75+
permissions: { project_access: projectAccess, group_access: groupAccess },
76+
} = await got
77+
.get(projectApiUrl, {
78+
headers: { [tokenHeader]: gitlabToken },
79+
...proxy,
80+
})
81+
.json());
82+
if (
83+
context.options.dryRun &&
84+
!((projectAccess && projectAccess.access_level >= 10) || (groupAccess && groupAccess.access_level >= 10))
85+
) {
86+
errors.push(getError("EGLNOPULLPERMISSION", { projectPath }));
87+
} else if (
88+
!((projectAccess && projectAccess.access_level >= 30) || (groupAccess && groupAccess.access_level >= 30))
89+
) {
90+
errors.push(getError("EGLNOPUSHPERMISSION", { projectPath }));
91+
}
8092
}
8193
} catch (error) {
8294
if (error.response && error.response.statusCode === 401) {

test/helpers/mock-gitlab.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,5 +22,5 @@ export default function (
2222
: null || '/api/v4',
2323
} = {}
2424
) {
25-
return nock(urlJoin(gitlabUrl, gitlabApiPathPrefix), {reqheaders: {'Private-Token': gitlabToken}});
25+
return nock(urlJoin(gitlabUrl, gitlabApiPathPrefix), {reqheaders: {[gitlabToken === env.CI_JOB_TOKEN ? "Job-Token" : "Private-Token"]: gitlabToken}});
2626
};

test/integration.test.js

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,3 +112,32 @@ test.serial("Verify Github auth and release", async (t) => {
112112
t.deepEqual(t.context.log.args[1], ["Published GitLab release: %s", nextRelease.gitTag]);
113113
t.true(gitlab.isDone());
114114
});
115+
116+
test.serial("Verify GitLab auth and release with Job Token", async (t) => {
117+
const env = { GL_TOKEN: "gitlab_token", CI_JOB_TOKEN: "gitlab_token" };
118+
const owner = "test_user";
119+
const repo = "test_repo";
120+
const options = { repositoryUrl: `https://github.com/${owner}/${repo}.git` };
121+
const encodedProjectPath = encodeURIComponent(`${owner}/${repo}`);
122+
const nextRelease = { gitHead: "123", gitTag: "v1.0.0", notes: "Test release note body" };
123+
124+
const gitlab = authenticate(env)
125+
.get(`/projects/${encodedProjectPath}/releases`)
126+
.reply(200, [])
127+
.post(`/projects/${encodedProjectPath}/releases`, {
128+
tag_name: nextRelease.gitTag,
129+
description: nextRelease.notes,
130+
assets: {
131+
links: [],
132+
},
133+
})
134+
.reply(200);
135+
136+
await t.notThrowsAsync(t.context.m.verifyConditions({ successCommentCondition: false, failCommentCondition: false }, { env, options, logger: t.context.logger }));
137+
const result = await t.context.m.publish({}, { env, options, nextRelease, logger: t.context.logger });
138+
139+
t.is(result.url, `https://gitlab.com/${owner}/${repo}/-/releases/${nextRelease.gitTag}`);
140+
t.deepEqual(t.context.log.args[0], ["Verify GitLab authentication (%s)", "https://gitlab.com/api/v4"]);
141+
t.deepEqual(t.context.log.args[1], ["Published GitLab release: %s", nextRelease.gitTag]);
142+
t.true(gitlab.isDone());
143+
});

test/resolve-config.test.js

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import resolveConfig from "../lib/resolve-config.js";
55

66
const defaultOptions = {
77
gitlabToken: undefined,
8+
isJobToken: false,
9+
tokenHeader: "PRIVATE-TOKEN",
810
gitlabUrl: "https://gitlab.com",
911
gitlabApiUrl: urlJoin("https://gitlab.com", "/api/v4"),
1012
assets: undefined,
@@ -508,3 +510,23 @@ test("Ignore GitLab CI/CD environment variables if not running on GitLab CI/CD",
508510
}
509511
);
510512
});
513+
514+
test("Use job token when GitLab token equals CI_JOB_TOKEN", (t) => {
515+
const jobToken = "TOKEN"
516+
517+
t.deepEqual(
518+
resolveConfig(
519+
{},
520+
{
521+
envCi: { service: "gitlab" },
522+
env: { GL_TOKEN: jobToken, CI_JOB_TOKEN: jobToken },
523+
}
524+
),
525+
{
526+
...defaultOptions,
527+
gitlabToken: jobToken,
528+
isJobToken: true,
529+
tokenHeader: "JOB-TOKEN",
530+
}
531+
);
532+
});

0 commit comments

Comments
 (0)