Skip to content

Add support for publishing GitLab CI components to catalog #883

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 3 commits into
base: master
Choose a base branch
from
Draft
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
23 changes: 23 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,28 @@ The plugin can be configured in the [**semantic-release** configuration file](ht

With this example [GitLab releases](https://docs.gitlab.com/ee/user/project/releases/) will be published to the `https://custom.gitlab.com` instance.

### GitLab CI Component Example

For projects that are [GitLab CI components](https://docs.gitlab.com/ee/ci/components/) and need to be published to the GitLab catalog:

```json
{
"branches": ["main"],
"plugins": [
"@semantic-release/commit-analyzer",
"@semantic-release/release-notes-generator",
[
"@semantic-release/gitlab",
{
"publishToCatalog": true
}
]
]
}
```

This will create a GitLab release and then publish the component to the GitLab catalog using the `/projects/:id/catalog/publish` API endpoint.

## Configuration

### GitLab authentication
Expand Down Expand Up @@ -95,6 +117,7 @@ If you need to bypass the proxy for some hosts, configure the `NO_PROXY` environ
| `failCommentCondition` | Use this as condition, when to comment on or create an issues in case of failures. See [failCommentCondition](#failCommentCondition). | - |
| `labels` | The [labels](https://docs.gitlab.com/ee/user/project/labels.html#labels) to add to the issue created when a release fails. Set to `false` to not add any label. Labels should be comma-separated as described in the [official docs](https://docs.gitlab.com/ee/api/issues.html#new-issue), e.g. `"semantic-release,bot"`. | `semantic-release` |
| `assignee` | The [assignee](https://docs.gitlab.com/ee/user/project/issues/managing_issues.html#assignee) to add to the issue created when a release fails. | - |
| `publishToCatalog` | Whether to publish GitLab CI components to the [GitLab catalog](https://docs.gitlab.com/ee/ci/components/) after creating a release. When enabled, an additional API call to `/projects/:id/catalog/publish` is made after the release is created. This is only needed for projects that are GitLab CI components. | `false` |
| `retryLimit` | The maximum number of retries for failing HTTP requests. | `3` |

#### assets
Expand Down
6 changes: 6 additions & 0 deletions lib/definitions/errors.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,12 @@ Your configuration for the \`labels\` option is \`${stringify(labels)}\`.`,

Your configuration for the \`assignee\` option is \`${stringify(assignee)}\`.`,
}),
EINVALIDPUBLISHTOCATALOG: ({publishToCatalog}) => ({
message: 'Invalid `publishToCatalog` option.',
details: `The [publishToCatalog option](${linkify('README.md#publishtocatalog')}) if defined, must be a \`Boolean\`.

Your configuration for the \`publishToCatalog\` option is \`${stringify(publishToCatalog)}\`.`,
}),
EINVALIDGITLABURL: () => ({
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\`.
Expand Down
31 changes: 29 additions & 2 deletions lib/publish.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,17 @@ export default async (pluginConfig, context) => {
nextRelease: { gitTag, gitHead, notes, version },
logger,
} = context;
const { gitlabToken, gitlabUrl, gitlabApiUrl, assets, milestones, proxy, retryLimit, retryStatusCodes } =
resolveConfig(pluginConfig, context);
const {
gitlabToken,
gitlabUrl,
gitlabApiUrl,
assets,
milestones,
proxy,
retryLimit,
retryStatusCodes,
publishToCatalog,
} = resolveConfig(pluginConfig, context);
const assetsList = [];
const { projectPath, projectApiUrl } = getProjectContext(context, gitlabUrl, gitlabApiUrl, repositoryUrl);

Expand Down Expand Up @@ -197,6 +206,24 @@ export default async (pluginConfig, context) => {

logger.log("Published GitLab release: %s", gitTag);

// Publish to catalog if enabled
if (publishToCatalog) {
debug("Publishing to GitLab catalog for project: %s", projectPath);

const catalogPublishEndpoint = urlJoin(projectApiUrl, "catalog/publish");

try {
await got.post(catalogPublishEndpoint, {
...apiOptions,
...proxy,
});
logger.log("Published GitLab CI component to catalog: %s", gitTag);
} catch (error) {
logger.error("An error occurred while publishing to the GitLab catalog API:\n%O", error);
throw error;
}
}

const releaseUrl = urlJoin(gitlabUrl, projectPath, `/-/releases/${encodedGitTag}`);

return { name: RELEASE_NAME, url: releaseUrl };
Expand Down
2 changes: 2 additions & 0 deletions lib/resolve-config.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export default (
labels,
assignee,
retryLimit,
publishToCatalog,
},
{
envCi: { service } = {},
Expand Down Expand Up @@ -71,6 +72,7 @@ export default (
assignee,
retryLimit: retryLimit ?? DEFAULT_RETRY_LIMIT,
retryStatusCodes: DEFAULT_RETRY_STATUS_CODES,
publishToCatalog: publishToCatalog || false,
};
};

Expand Down
1 change: 1 addition & 0 deletions lib/verify.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ const VALIDATORS = {
failComment: canBeDisabled(isNonEmptyString),
labels: canBeDisabled(isNonEmptyString),
assignee: isNonEmptyString,
publishToCatalog: (value) => typeof value === "boolean",
};

export default async (pluginConfig, context) => {
Expand Down
56 changes: 56 additions & 0 deletions test/publish.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -666,3 +666,59 @@ test.serial("Publish a release with error response", async (t) => {
t.is(error.message, `Response code 499 (Something went wrong)`);
t.true(gitlab.isDone());
});

test.serial("Publish a release with catalog publishing enabled", async (t) => {
const owner = "test_user";
const repo = "test_repo";
const env = { GITLAB_TOKEN: "gitlab_token" };
const pluginConfig = { publishToCatalog: true };
const nextRelease = { gitHead: "123", gitTag: "v1.0.0", notes: "Test release note body" };
const options = { repositoryUrl: `https://gitlab.com/${owner}/${repo}.git` };
const encodedProjectPath = encodeURIComponent(`${owner}/${repo}`);
const encodedGitTag = encodeURIComponent(nextRelease.gitTag);

const gitlab = authenticate(env)
.post(`/projects/${encodedProjectPath}/releases`, {
tag_name: nextRelease.gitTag,
description: nextRelease.notes,
assets: {
links: [],
},
})
.reply(200)
.post(`/projects/${encodedProjectPath}/catalog/publish`)
.reply(200);

const result = await publish(pluginConfig, { env, options, nextRelease, logger: t.context.logger });

t.is(result.url, `https://gitlab.com/${owner}/${repo}/-/releases/${encodedGitTag}`);
t.deepEqual(t.context.log.args[0], ["Published GitLab release: %s", nextRelease.gitTag]);
t.deepEqual(t.context.log.args[1], ["Published GitLab CI component to catalog: %s", nextRelease.gitTag]);
t.true(gitlab.isDone());
});

test.serial("Publish a release with catalog publishing error", async (t) => {
const owner = "test_user";
const repo = "test_repo";
const env = { GITLAB_TOKEN: "gitlab_token" };
const pluginConfig = { publishToCatalog: true };
const nextRelease = { gitHead: "123", gitTag: "v1.0.0", notes: "Test release note body" };
const options = { repositoryUrl: `https://gitlab.com/${owner}/${repo}.git` };
const encodedProjectPath = encodeURIComponent(`${owner}/${repo}`);

const gitlab = authenticate(env)
.post(`/projects/${encodedProjectPath}/releases`, {
tag_name: nextRelease.gitTag,
description: nextRelease.notes,
assets: {
links: [],
},
})
.reply(200)
.post(`/projects/${encodedProjectPath}/catalog/publish`)
.reply(400, { message: "Catalog publishing failed" });

const error = await t.throwsAsync(publish(pluginConfig, { env, options, nextRelease, logger: t.context.logger }));
t.is(error.message, `Response code 400 (Catalog publishing failed)`);
t.true(gitlab.isDone());
});
1 change: 1 addition & 0 deletions test/resolve-config.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ const defaultOptions = {
proxy: {},
retryLimit: 3,
retryStatusCodes: [408, 413, 422, 429, 500, 502, 503, 504, 521, 522, 524],
publishToCatalog: false,
};

test("Returns user config", (t) => {
Expand Down
40 changes: 40 additions & 0 deletions test/verify.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -988,3 +988,43 @@ test.serial(
t.true(gitlab.isDone());
}
);

test.serial('Does not throw SemanticReleaseError if "publishToCatalog" option is valid boolean', async (t) => {
const owner = "test_user";
const repo = "test_repo";
const env = { GITLAB_TOKEN: "gitlab_token" };
const publishToCatalog = true;
const gitlab = authenticate(env)
.get(`/projects/${owner}%2F${repo}`)
.reply(200, { permissions: { project_access: { access_level: 40 } } });

await t.notThrowsAsync(
verify(
{ publishToCatalog },
{ env, options: { repositoryUrl: `https://gitlab.com/${owner}/${repo}.git` }, logger: t.context.logger }
)
);
t.true(gitlab.isDone());
});

test.serial('Throw SemanticReleaseError if "publishToCatalog" option is not a boolean', async (t) => {
const owner = "test_user";
const repo = "test_repo";
const env = { GITLAB_TOKEN: "gitlab_token" };
const publishToCatalog = "invalid";
const gitlab = authenticate(env)
.get(`/projects/${owner}%2F${repo}`)
.reply(200, { permissions: { project_access: { access_level: 40 } } });

const {
errors: [error],
} = await t.throwsAsync(
verify(
{ publishToCatalog },
{ env, options: { repositoryUrl: `https://gitlab.com/${owner}/${repo}.git` }, logger: t.context.logger }
)
);
t.is(error.name, "SemanticReleaseError");
t.is(error.code, "EINVALIDPUBLISHTOCATALOG");
t.true(gitlab.isDone());
});