Skip to content

Commit fdd0953

Browse files
authored
Merge pull request #919 from arvest-bjoneson/feat/job-token
2 parents 44dfb5c + c897291 commit fdd0953

11 files changed

Lines changed: 166 additions & 30 deletions

File tree

.github/workflows/test.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ jobs:
3131
cache: npm
3232
- run: npm ci
3333
- name: Ensure dependencies are compatible with the version of node
34-
run: npx ls-engines
34+
run: npx ls-engines@v0.9.4
3535
- run: npm run test
3636
test:
3737
runs-on: ubuntu-latest

README.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,13 +58,26 @@ Create a [project access token](https://docs.gitlab.com/user/project/settings/pr
5858

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

61+
#### Using a CI Job Token
62+
63+
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:
64+
65+
```json
66+
{
67+
"plugins": [["@semantic-release/gitlab", { "useJobToken": true }]]
68+
}
69+
```
70+
71+
> **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.
72+
6173
### Environment variables
6274

6375
| Variable | Description |
6476
| ------------------------------ | ------------------------------------------------------------------------------------------ |
6577
| `GL_TOKEN` or `GITLAB_TOKEN` | **Required.** The token used to authenticate with GitLab. |
6678
| `GL_URL` or `GITLAB_URL` | The GitLab endpoint. |
6779
| `GL_PREFIX` or `GITLAB_PREFIX` | The GitLab API prefix. |
80+
| `CI_JOB_TOKEN` | The GitLab CI/CD job token. Used if `useJobToken` is `true`. |
6881
| `HTTP_PROXY` or `HTTPS_PROXY` | HTTP or HTTPS proxy to use. |
6982
| `NO_PROXY` | Patterns for which the proxy should be ignored. See [details below](#proxy-configuration). |
7083

@@ -86,6 +99,7 @@ If you need to bypass the proxy for some hosts, configure the `NO_PROXY` environ
8699
| ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
87100
| `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`. |
88101
| `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`. |
102+
| `useJobToken` | Set to `true` to use the `CI_JOB_TOKEN` for authentication within a GitLab CI/CD environment. | `false` |
89103
| `assets` | An array of files to upload to the release. See [assets](#assets). | - |
90104
| `milestones` | An array of milestone titles to associate to the release. See [GitLab Release API](https://docs.gitlab.com/api/releases/#create-a-release). | - |
91105
| `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) |

lib/fail.js

Lines changed: 8 additions & 2 deletions
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,
@@ -25,11 +26,12 @@ export default async (pluginConfig, context) => {
2526
assignee,
2627
retryLimit,
2728
retryStatusCodes,
29+
useJobToken,
2830
} = resolveConfig(pluginConfig, context);
2931
const { encodedProjectPath, projectApiUrl } = getProjectContext(context, gitlabUrl, gitlabApiUrl, repositoryUrl);
3032

3133
const apiOptions = {
32-
headers: { "PRIVATE-TOKEN": gitlabToken },
34+
headers: { [tokenHeader]: gitlabToken },
3335
retry: {
3436
limit: retryLimit,
3537
statusCodes: retryStatusCodes,
@@ -41,7 +43,11 @@ export default async (pluginConfig, context) => {
4143
logger.error(`Failure reporting should be disabled via 'failCommentCondition'.
4244
Using 'false' for 'failComment' or 'failTitle' is deprecated and will be removed in a future major version.`);
4345
} else if (failCommentCondition === false) {
44-
logger.log("Skip issue creation.");
46+
logger.log(
47+
"Skip issue creation." + useJobToken
48+
? " Setting 'failComment' or 'failTitle' has no effect when 'useJobToken' is set."
49+
: ""
50+
);
4551
} else {
4652
const encodedFailTitle = encodeURIComponent(failTitle);
4753
const description = failComment ? template(failComment)({ branch, errors }) : getFailComment(branch, errors);

lib/publish.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,15 +22,15 @@ 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);
2929

3030
const encodedGitTag = encodeURIComponent(gitTag);
3131
const apiOptions = {
3232
headers: {
33-
"PRIVATE-TOKEN": gitlabToken,
33+
[tokenHeader]: gitlabToken,
3434
},
3535
hooks: {
3636
beforeError: [

lib/resolve-config.js

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,15 @@ export default (
1616
labels,
1717
assignee,
1818
retryLimit,
19+
useJobToken,
1920
},
2021
{
2122
envCi: { service } = {},
2223
env: {
2324
CI_PROJECT_URL,
2425
CI_PROJECT_PATH,
2526
CI_API_V4_URL,
27+
CI_JOB_TOKEN,
2628
GL_TOKEN,
2729
GITLAB_TOKEN,
2830
GL_URL,
@@ -51,7 +53,9 @@ export default (
5153
? CI_PROJECT_URL.replace(new RegExp(`/${CI_PROJECT_PATH}$`), "")
5254
: "https://gitlab.com");
5355
return {
54-
gitlabToken: GL_TOKEN || GITLAB_TOKEN,
56+
gitlabToken: useJobToken ? CI_JOB_TOKEN : GL_TOKEN || GITLAB_TOKEN,
57+
tokenHeader: useJobToken ? "JOB-TOKEN" : "PRIVATE-TOKEN",
58+
useJobToken,
5559
gitlabUrl: defaultedGitlabUrl,
5660
gitlabApiUrl:
5761
userGitlabUrl && userGitlabApiPathPrefix
@@ -62,11 +66,11 @@ export default (
6266
assets: assets ? castArray(assets) : assets,
6367
milestones: milestones ? castArray(milestones) : milestones,
6468
successComment,
65-
successCommentCondition,
69+
successCommentCondition: useJobToken ? false : successCommentCondition,
6670
proxy: getProxyConfiguration(defaultedGitlabUrl, HTTP_PROXY, HTTPS_PROXY, NO_PROXY),
6771
failTitle: isNil(failTitle) ? "The automated release is failing 🚨" : failTitle,
6872
failComment,
69-
failCommentCondition,
73+
failCommentCondition: useJobToken ? false : failCommentCondition,
7074
labels: isNil(labels) ? "semantic-release" : labels === false ? false : labels,
7175
assignee,
7276
retryLimit: retryLimit ?? DEFAULT_RETRY_LIMIT,

lib/success.js

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,17 +17,19 @@ export default async (pluginConfig, context) => {
1717
} = context;
1818
const {
1919
gitlabToken,
20+
tokenHeader,
2021
gitlabUrl,
2122
gitlabApiUrl,
2223
successComment,
2324
successCommentCondition,
2425
proxy,
2526
retryLimit,
2627
retryStatusCodes,
28+
useJobToken,
2729
} = resolveConfig(pluginConfig, context);
2830
const { projectApiUrl } = getProjectContext(context, gitlabUrl, gitlabApiUrl, repositoryUrl);
2931
const apiOptions = {
30-
headers: { "PRIVATE-TOKEN": gitlabToken },
32+
headers: { [tokenHeader]: gitlabToken },
3133
retry: { limit: retryLimit, statusCodes: retryStatusCodes },
3234
};
3335

@@ -36,7 +38,11 @@ export default async (pluginConfig, context) => {
3638
logger.error(`Issue and pull request comments should be disabled via 'successCommentCondition'.
3739
Using 'false' for 'successComment' is deprecated and will be removed in a future major version.`);
3840
} else if (successCommentCondition === false) {
39-
logger.log("Skip commenting on issues and pull requests.");
41+
logger.log(
42+
"Skip commenting on issues and pull requests." + useJobToken
43+
? " Setting 'successComment' has no effect when 'useJobToken' is set."
44+
: ""
45+
);
4046
} else {
4147
const releaseInfos = releases.filter((release) => Boolean(release.name));
4248
try {

lib/verify.js

Lines changed: 27 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, gitlabUrl, gitlabApiUrl, tokenHeader, useJobToken, proxy, ...options } = resolveConfig(
35+
pluginConfig,
36+
context
37+
);
3438
const { projectPath, projectApiUrl } = getProjectContext(context, gitlabUrl, gitlabApiUrl, repositoryUrl);
3539

3640
debug("apiUrl: %o", gitlabApiUrl);
@@ -60,23 +64,28 @@ export default async (pluginConfig, context) => {
6064
logger.log("Verify GitLab authentication (%s)", gitlabApiUrl);
6165

6266
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 }));
67+
if (useJobToken) {
68+
logger.log("Using Job Token for authentication. Some functionality may be disabled.");
69+
await got.get(urlJoin(projectApiUrl, "releases"), { headers: { [tokenHeader]: gitlabToken } });
70+
} else {
71+
({
72+
permissions: { project_access: projectAccess, group_access: groupAccess },
73+
} = await got
74+
.get(projectApiUrl, {
75+
headers: { [tokenHeader]: gitlabToken },
76+
...proxy,
77+
})
78+
.json());
79+
if (
80+
context.options.dryRun &&
81+
!((projectAccess && projectAccess.access_level >= 10) || (groupAccess && groupAccess.access_level >= 10))
82+
) {
83+
errors.push(getError("EGLNOPULLPERMISSION", { projectPath }));
84+
} else if (
85+
!((projectAccess && projectAccess.access_level >= 30) || (groupAccess && groupAccess.access_level >= 30))
86+
) {
87+
errors.push(getError("EGLNOPUSHPERMISSION", { projectPath }));
88+
}
8089
}
8190
} catch (error) {
8291
if (error.response && error.response.statusCode === 401) {

test/helpers/mock-gitlab.js

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import urlJoin from 'url-join';
1313
export default function (
1414
env = {},
1515
{
16+
useJobToken = false,
1617
gitlabToken = env.GL_TOKEN || env.GITLAB_TOKEN || 'GL_TOKEN',
1718
gitlabUrl = env.GL_URL || env.GITLAB_URL || 'https://gitlab.com',
1819
gitlabApiPathPrefix = typeof env.GL_PREFIX === 'string'
@@ -22,5 +23,8 @@ export default function (
2223
: null || '/api/v4',
2324
} = {}
2425
) {
25-
return nock(urlJoin(gitlabUrl, gitlabApiPathPrefix), {reqheaders: {'Private-Token': gitlabToken}});
26-
};
26+
const tokenHeader = useJobToken ? "JOB-TOKEN" : "Private-Token";
27+
const token = useJobToken ? env.CI_JOB_TOKEN : gitlabToken;
28+
29+
return nock(urlJoin(gitlabUrl, gitlabApiPathPrefix), { reqheaders: { [tokenHeader]: token } });
30+
}

test/integration.test.js

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,3 +112,34 @@ 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 = { CI_JOB_TOKEN: "job_token" };
118+
const owner = "test_user";
119+
const repo = "test_repo";
120+
const options = { repositoryUrl: `https://gitlab.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+
const pluginConfig = { useJobToken: true };
124+
125+
const gitlab = authenticate(env, { useJobToken: true })
126+
.get(`/projects/${encodedProjectPath}/releases`)
127+
.reply(200)
128+
.post(`/projects/${encodedProjectPath}/releases`, {
129+
tag_name: nextRelease.gitTag,
130+
description: nextRelease.notes,
131+
assets: {
132+
links: [],
133+
},
134+
})
135+
.reply(200, {});
136+
137+
await t.notThrowsAsync(t.context.m.verifyConditions(pluginConfig, { env, options, logger: t.context.logger }));
138+
const result = await t.context.m.publish(pluginConfig, { env, options, nextRelease, logger: t.context.logger });
139+
140+
t.is(result.url, `https://gitlab.com/${owner}/${repo}/-/releases/${nextRelease.gitTag}`);
141+
t.deepEqual(t.context.log.args[0], ["Verify GitLab authentication (%s)", "https://gitlab.com/api/v4"]);
142+
t.deepEqual(t.context.log.args[1], ["Using Job Token for authentication. Some functionality may be disabled."]);
143+
t.deepEqual(t.context.log.args[2], ["Published GitLab release: %s", nextRelease.gitTag]);
144+
t.true(gitlab.isDone());
145+
});

test/resolve-config.test.js

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ const defaultOptions = {
1616
failCommentCondition: undefined,
1717
labels: "semantic-release",
1818
assignee: undefined,
19+
tokenHeader: "PRIVATE-TOKEN",
20+
useJobToken: undefined,
1921
proxy: {},
2022
retryLimit: 3,
2123
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",
508510
}
509511
);
510512
});
513+
514+
test("Set token header to JOB-TOKEN when useJobToken is set to true", (t) => {
515+
const jobToken = "TOKEN";
516+
517+
t.deepEqual(
518+
resolveConfig(
519+
{ useJobToken: true },
520+
{
521+
envCi: { service: "gitlab" },
522+
env: { CI_JOB_TOKEN: jobToken },
523+
}
524+
),
525+
{
526+
...defaultOptions,
527+
gitlabToken: jobToken,
528+
useJobToken: true,
529+
tokenHeader: "JOB-TOKEN",
530+
successCommentCondition: false,
531+
failCommentCondition: false,
532+
}
533+
);
534+
});

0 commit comments

Comments
 (0)