diff --git a/README.md b/README.md index 52c34296..1a19966f 100644 --- a/README.md +++ b/README.md @@ -96,6 +96,7 @@ If you need to bypass the proxy for some hosts, configure the `NO_PROXY` environ | `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. | - | | `retryLimit` | The maximum number of retries for failing HTTP requests. | `3` | +| `publishToCatalog` | [EXPERIMENTAL] Publishes CI/CD components to the catalog. See [publishToCatalog](#publishToCatalog). | `false` | #### assets @@ -211,6 +212,14 @@ The fail comment condition is generated with [Lodash template](https://lodash.co > check the [GitLab API Issue object](https://docs.gitlab.com/ee/api/issues.html#single-issue) for properties which can be used for the filter +#### publishToCatalog + +**Note**: This is an EXPERIMENTAL option that might change in the future. It depends on a GitLab feature flag that, as of May 2025, is only enabled for a subset of projects on GitLab.com. Follow the [upstream issue](https://gitlab.com/gitlab-org/gitlab/-/issues/463253) for progress. + +Use this option to [publish CI/CD components to the catalog](https://gitlab.com/gitlab-org/cli/-/blob/main/docs/source/repo/publish/catalog.md) as part of the release process. + +The publishing is done via the `glab` CLI, so make sure to [install it before](https://gitlab.com/gitlab-org/cli#installation). + ## Compatibility The latest version of this plugin is compatible with all currently-supported versions of GitLab, [which is the current major version and previous two major versions](https://about.gitlab.com/support/statement-of-support.html#version-support). This plugin is not guaranteed to work with unsupported versions of GitLab. diff --git a/lib/definitions/errors.js b/lib/definitions/errors.js index 51508538..7c9e8fcc 100644 --- a/lib/definitions/errors.js +++ b/lib/definitions/errors.js @@ -87,4 +87,8 @@ Please make sure the GitLab user associated with the token has the [permission t 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}.`, }), + EGLABNOTINSTALLED: () => ({ + message: 'GitLab CLI not installed.', + details: 'The [GitLab CLI needs to be installed](https://gitlab.com/gitlab-org/cli#installation) so that the project\'s CI components can be published.', + }), }; diff --git a/lib/glab.js b/lib/glab.js new file mode 100644 index 00000000..82fb2b90 --- /dev/null +++ b/lib/glab.js @@ -0,0 +1,5 @@ +import { execa } from "execa"; + +export default async (args, options) => { + return execa("glab", args, options); +}; diff --git a/lib/publish.js b/lib/publish.js index 4a8f9511..a2ecec7e 100644 --- a/lib/publish.js +++ b/lib/publish.js @@ -12,7 +12,7 @@ import resolveConfig from "./resolve-config.js"; import getAssets from "./glob-assets.js"; import { RELEASE_NAME } from "./definitions/constants.js"; import getProjectContext from "./get-project-context.js"; - +import glab from "./glab.js"; const isUrlScheme = (value) => /^(https|http|ftp):\/\//.test(value); export default async (pluginConfig, context) => { @@ -22,17 +22,24 @@ 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); const encodedGitTag = encodeURIComponent(gitTag); const encodedVersion = encodeURIComponent(version); const apiOptions = { - headers: { - "PRIVATE-TOKEN": gitlabToken, - }, + headers: { "PRIVATE-TOKEN": gitlabToken }, hooks: { beforeError: [ (error) => { @@ -185,11 +192,7 @@ export default async (pluginConfig, context) => { debug("POST-ing the following JSON to %s:\n%s", createReleaseEndpoint, JSON.stringify(json, null, 2)); try { - await got.post(createReleaseEndpoint, { - ...apiOptions, - ...proxy, - json, - }); + await got.post(createReleaseEndpoint, { ...apiOptions, ...proxy, json }); } catch (error) { logger.error("An error occurred while making a request to the GitLab release API:\n%O", error); throw error; @@ -199,5 +202,19 @@ export default async (pluginConfig, context) => { const releaseUrl = urlJoin(gitlabUrl, projectPath, `/-/releases/${encodedGitTag}`); + if (publishToCatalog) { + try { + await glab(["repo", "publish", "catalog", gitTag], { + cwd, + timeout: 30 * 1000, + env: { GITLAB_TOKEN: gitlabToken }, + }); + logger.log("Published tag %s to the CI catalog", gitTag); + } catch (error) { + logger.error("An error occurred while publishing tag %s to the CI catalog:\n%O", gitTag, error); + throw error; + } + } + return { name: RELEASE_NAME, url: releaseUrl }; }; diff --git a/lib/resolve-config.js b/lib/resolve-config.js index d26dbf37..fab271ab 100644 --- a/lib/resolve-config.js +++ b/lib/resolve-config.js @@ -16,6 +16,7 @@ export default ( labels, assignee, retryLimit, + publishToCatalog, }, { envCi: { service } = {}, @@ -71,6 +72,7 @@ export default ( assignee, retryLimit: retryLimit ?? DEFAULT_RETRY_LIMIT, retryStatusCodes: DEFAULT_RETRY_STATUS_CODES, + publishToCatalog: publishToCatalog ?? false, }; }; diff --git a/lib/verify.js b/lib/verify.js index fdfc3b7f..0a0f7df7 100644 --- a/lib/verify.js +++ b/lib/verify.js @@ -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 glab from "./glab.js"; const isNonEmptyString = (value) => isString(value) && value.trim(); const isStringOrStringArray = (value) => @@ -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, proxy, publishToCatalog, ...options } = resolveConfig( + pluginConfig, + context + ); const { projectPath, projectApiUrl } = getProjectContext(context, gitlabUrl, gitlabApiUrl, repositoryUrl); debug("apiUrl: %o", gitlabApiUrl); @@ -89,6 +93,18 @@ export default async (pluginConfig, context) => { } } + if (publishToCatalog === true) { + try { + logger.log("Verifying that the GitLab CLI is installed"); + await glab(["version"], { + timeout: 5 * 1000, + }); + } catch (error) { + logger.error("The GitLab CLI is required but failed to run:\n%O", error); + errors.push(getError("EGLABNOTINSTALLED")); + } + } + if (errors.length > 0) { throw new AggregateError(errors); } diff --git a/package-lock.json b/package-lock.json index b96aedd3..e7c0f72c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,6 +14,7 @@ "debug": "^4.0.0", "dir-glob": "^3.0.0", "escape-string-regexp": "^5.0.0", + "execa": "^9.5.2", "formdata-node": "^6.0.3", "fs-extra": "^11.0.0", "globby": "^14.0.0", @@ -30,7 +31,8 @@ "prettier": "3.5.3", "semantic-release": "24.2.5", "sinon": "20.0.0", - "tempy": "1.0.1" + "tempy": "1.0.1", + "testdouble": "^3.20.2" }, "engines": { "node": ">=20.8.1" @@ -2138,7 +2140,6 @@ "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "dev": true, "license": "MIT", "dependencies": { "path-key": "^3.1.0", @@ -2796,7 +2797,6 @@ "version": "9.5.2", "resolved": "https://registry.npmjs.org/execa/-/execa-9.5.2.tgz", "integrity": "sha512-EHlpxMCpHWSAh1dgS6bVeoLAXGnJNdR93aabr4QCGbzOM73o5XmRfM/e5FUqsw3aagP8S8XEWUWFAxnRBnAF0Q==", - "dev": true, "license": "MIT", "dependencies": { "@sindresorhus/merge-streams": "^4.0.0", @@ -2823,7 +2823,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-4.0.0.tgz", "integrity": "sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=18" @@ -2885,7 +2884,6 @@ "version": "6.1.0", "resolved": "https://registry.npmjs.org/figures/-/figures-6.1.0.tgz", "integrity": "sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==", - "dev": true, "license": "MIT", "dependencies": { "is-unicode-supported": "^2.0.0" @@ -3462,7 +3460,6 @@ "version": "8.0.1", "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-8.0.1.tgz", "integrity": "sha512-eKCa6bwnJhvxj14kZk5NCPc6Hb6BdsU9DZcOnmQKSnO1VKrfV0zCvtttPZUsBvjmNDn8rpcJfpwSYnHBjc95MQ==", - "dev": true, "license": "Apache-2.0", "engines": { "node": ">=18.18.0" @@ -3647,6 +3644,22 @@ "dev": true, "license": "MIT" }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -3731,7 +3744,6 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", - "dev": true, "license": "MIT", "engines": { "node": ">=12" @@ -3764,6 +3776,16 @@ "dev": true, "license": "MIT" }, + "node_modules/is-regexp": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-regexp/-/is-regexp-1.0.0.tgz", + "integrity": "sha512-7zjFAPO4/gwyQAAgRRmqeEeyIICSdmCqa3tsVHMdBzaXXRiqopZL4Cyghg/XulGWrtABTpbnYYzzIRffLkP4oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/is-stream": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-4.0.1.tgz", @@ -3780,7 +3802,6 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz", "integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=18" @@ -3800,7 +3821,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true, "license": "ISC" }, "node_modules/issue-parser": { @@ -4780,7 +4800,6 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-6.0.0.tgz", "integrity": "sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA==", - "dev": true, "license": "MIT", "dependencies": { "path-key": "^4.0.0", @@ -4797,7 +4816,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=12" @@ -7642,7 +7660,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/parse-ms/-/parse-ms-4.0.0.tgz", "integrity": "sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==", - "dev": true, "license": "MIT", "engines": { "node": ">=18" @@ -7727,12 +7744,18 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" } }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, "node_modules/path-scurry": { "version": "1.11.1", "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", @@ -7932,7 +7955,6 @@ "version": "9.2.0", "resolved": "https://registry.npmjs.org/pretty-ms/-/pretty-ms-9.2.0.tgz", "integrity": "sha512-4yf0QO/sllf/1zbZWYnvWw3NxCQwLXKzIj0G849LSufP15BXKM0rbD2Z3wVnkMfjdn/CB0Dpp444gYAACdsplg==", - "dev": true, "license": "MIT", "dependencies": { "parse-ms": "^4.0.0" @@ -8004,6 +8026,20 @@ ], "license": "MIT" }, + "node_modules/quibble": { + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/quibble/-/quibble-0.9.2.tgz", + "integrity": "sha512-BrL7hrZcbyyt5ZDfePkGFDc3m82uUtxCPOnpRUrkOdtBnmV9ldQKxXORkKL8eIzToRNaCpIPyKyfdfq/tBlFAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "lodash": "^4.17.21", + "resolve": "^1.22.8" + }, + "engines": { + "node": ">= 0.14.0" + } + }, "node_modules/quick-lru": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", @@ -8140,6 +8176,27 @@ "node": ">=0.10.0" } }, + "node_modules/resolve": { + "version": "1.22.10", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", + "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/resolve-alpn": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz", @@ -8446,7 +8503,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, "license": "MIT", "dependencies": { "shebang-regex": "^3.0.0" @@ -8459,7 +8515,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -8469,7 +8524,6 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "dev": true, "license": "ISC", "engines": { "node": ">=14" @@ -8839,6 +8893,30 @@ "node": ">=8" } }, + "node_modules/stringify-object-es5": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/stringify-object-es5/-/stringify-object-es5-2.5.0.tgz", + "integrity": "sha512-vE7Xdx9ylG4JI16zy7/ObKUB+MtxuMcWlj/WHHr3+yAlQoN6sst2stU9E+2Qs3OrlJw/Pf3loWxL1GauEHf6MA==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "is-plain-obj": "^1.0.0", + "is-regexp": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stringify-object-es5/node_modules/is-plain-obj": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz", + "integrity": "sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/strip-ansi": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", @@ -8893,7 +8971,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-4.0.0.tgz", "integrity": "sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw==", - "dev": true, "license": "MIT", "engines": { "node": ">=18" @@ -8999,6 +9076,19 @@ "url": "https://github.com/chalk/supports-hyperlinks?sponsor=1" } }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/symbol-tree": { "version": "3.2.4", "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", @@ -9152,6 +9242,22 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/testdouble": { + "version": "3.20.2", + "resolved": "https://registry.npmjs.org/testdouble/-/testdouble-3.20.2.tgz", + "integrity": "sha512-790e9vJKdfddWNOaxW1/V9FcMk48cPEl3eJSj2i8Hh1fX89qArEJ6cp3DBnaECpGXc3xKJVWbc1jeNlWYWgiMg==", + "dev": true, + "license": "MIT", + "dependencies": { + "lodash": "^4.17.21", + "quibble": "^0.9.2", + "stringify-object-es5": "^2.5.0", + "theredoc": "^1.0.0" + }, + "engines": { + "node": ">= 16" + } + }, "node_modules/thenify": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", @@ -9175,6 +9281,13 @@ "node": ">=0.8" } }, + "node_modules/theredoc": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/theredoc/-/theredoc-1.0.0.tgz", + "integrity": "sha512-KU3SA3TjRRM932jpNfD3u4Ec3bSvedyo5ITPI7zgWYnKep7BwQQaxlhI9qbO+lKJoRnoAbEVfMcAHRuKVYikDA==", + "dev": true, + "license": "MIT" + }, "node_modules/through2": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", @@ -9483,7 +9596,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, "license": "ISC", "dependencies": { "isexe": "^2.0.0" @@ -9872,7 +9984,6 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/yoctocolors/-/yoctocolors-2.1.1.tgz", "integrity": "sha512-GQHQqAopRhwU8Kt1DDM8NjibDXHC8eoh1erhGAJPEyveY9qqVeXvVikNKrDz69sHowPMorbPUrH/mx8c50eiBQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=18" diff --git a/package.json b/package.json index c50532d6..254d03fe 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "debug": "^4.0.0", "dir-glob": "^3.0.0", "escape-string-regexp": "^5.0.0", + "execa": "^9.5.2", "formdata-node": "^6.0.3", "fs-extra": "^11.0.0", "globby": "^14.0.0", @@ -38,7 +39,8 @@ "prettier": "3.5.3", "semantic-release": "24.2.5", "sinon": "20.0.0", - "tempy": "1.0.1" + "tempy": "1.0.1", + "testdouble": "^3.20.2" }, "engines": { "node": ">=20.8.1" diff --git a/test/publish.test.js b/test/publish.test.js index 249e8bcf..97ef06fb 100644 --- a/test/publish.test.js +++ b/test/publish.test.js @@ -4,9 +4,9 @@ import tempy from "tempy"; import { stub } from "sinon"; import publish from "../lib/publish.js"; import authenticate from "./helpers/mock-gitlab.js"; +import * as td from "testdouble"; /* eslint camelcase: ["error", {properties: "never"}] */ - test.beforeEach((t) => { // Mock logger t.context.log = stub(); @@ -15,8 +15,9 @@ test.beforeEach((t) => { }); test.afterEach.always(() => { - // Clear nock + // Clear nock and testdouble nock.cleanAll(); + td.reset(); }); test.serial("Publish a release", async (t) => { @@ -666,3 +667,75 @@ 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 to CI catalog", 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); + + const execa = (await td.replaceEsm("execa")).execa; + td.when( + execa("glab", ["repo", "publish", "catalog", nextRelease.gitTag], { + cwd: undefined, + timeout: 30000, + env: { GITLAB_TOKEN: env.GITLAB_TOKEN }, + }) + ).thenResolve(); + const publishWithMockExeca = (await import("../lib/publish.js")).default; + await publishWithMockExeca(pluginConfig, { env, options, nextRelease, logger: t.context.logger }); + t.deepEqual(t.context.log.args[1], ["Published tag %s to the CI catalog", nextRelease.gitTag]); + t.true(gitlab.isDone()); +}); + +test.serial("Publish a release to CI catalog with 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); + + const execa = (await td.replaceEsm("execa")).execa; + const execaError = new Error("test"); + td.when( + execa("glab", ["repo", "publish", "catalog", nextRelease.gitTag], { + cwd: undefined, + timeout: 30000, + env: { GITLAB_TOKEN: env.GITLAB_TOKEN }, + }) + ).thenReject(execaError); + const publishWithMockedExeca = (await import("../lib/publish.js")).default; + const error = await t.throwsAsync( + publishWithMockedExeca(pluginConfig, { env, options, nextRelease, logger: t.context.logger }) + ); + t.deepEqual(t.context.error.args[0], [ + "An error occurred while publishing tag %s to the CI catalog:\n%O", + nextRelease.gitTag, + execaError, + ]); + t.is(error.message, execaError.message); + t.true(gitlab.isDone()); +}); diff --git a/test/resolve-config.test.js b/test/resolve-config.test.js index 5f800e78..97e892b9 100644 --- a/test/resolve-config.test.js +++ b/test/resolve-config.test.js @@ -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) => { diff --git a/test/verify.test.js b/test/verify.test.js index 48f3004f..5fdb6f27 100644 --- a/test/verify.test.js +++ b/test/verify.test.js @@ -3,6 +3,7 @@ import nock from "nock"; import { stub } from "sinon"; import verify from "../lib/verify.js"; import authenticate from "./helpers/mock-gitlab.js"; +import * as td from "testdouble"; /* eslint camelcase: ["error", {properties: "never"}] */ @@ -988,3 +989,34 @@ test.serial( t.true(gitlab.isDone()); } ); + +test.serial( + 'Throw SemanticReleaseError if "publishToCatalog" option is set and the GitLab CLI is not installed.', + async (t) => { + const owner = "test_user"; + const repo = "test_repo"; + const env = { GITLAB_TOKEN: "gitlab_token" }; + const gitlab = authenticate(env) + .get(`/projects/${owner}%2F${repo}`) + .reply(200, { permissions: { project_access: { access_level: 40 } } }); + + const execa = (await td.replaceEsm("execa")).execa; + td.when( + execa("glab", ["version"], { + timeout: 5000, + }) + ).thenReject(); + const verifyWithMockExeca = (await import("../lib/verify.js")).default; + const { + errors: [error], + } = await t.throwsAsync( + verifyWithMockExeca( + { publishToCatalog: true }, + { env, options: { repositoryUrl: `https://gitlab.com/${owner}/${repo}.git` }, logger: t.context.logger } + ) + ); + t.is(error.name, "SemanticReleaseError"); + t.is(error.code, "EGLABNOTINSTALLED"); + t.true(gitlab.isDone()); + } +);