diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 00000000..ef874a1e --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,21 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "type": "node", + "name": "Debug Tests", + "request": "launch", + "args": [ + "-v" + ], + "cwd": "${workspaceFolder}", + "console": "integratedTerminal", + "internalConsoleOptions": "neverOpen", + "disableOptimisticBPs": true, + "program": "${workspaceFolder}/node_modules/ava/cli.js" + } + ] +} \ No newline at end of file diff --git a/README.md b/README.md index 7dc60f5a..74d55fae 100644 --- a/README.md +++ b/README.md @@ -31,6 +31,9 @@ The plugin can be configured in the [**semantic-release** configuration file](ht "assets": [ {"path": "dist/asset.min.css", "label": "CSS distribution"}, {"path": "dist/asset.min.js", "label": "JS distribution"} + ], + "generics": [ + {"path": "dist/app.js", "label": "App"} ] }], ] @@ -63,6 +66,7 @@ Create a [personal access token](https://docs.gitlab.com/ce/user/profile/persona | `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/ee/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/ee/ci) or `/api/v4`. | | `assets` | An array of files to upload to the release. See [assets](#assets). | - | +| `generics` | An array of files to upload as part of a generic package to the release. See [generics](#generics). | - | | `milestones` | An array of milestone titles to associate to the release. See [GitLab Release API](https://docs.gitlab.com/ee/api/releases/#create-a-release). | - | #### assets @@ -100,6 +104,24 @@ distribution` and `MyLibrary CSS distribution` in the GitLab release. `css` files in the `dist` directory and its sub-directories excluding the minified version, plus the `build/MyLibrary.zip` file and label it `MyLibrary` in the GitLab release. +#### generics + +Can be a `Array` of `String`s with the direct file path or a list of `Object`s with the following properties: + +| Property | Description | Default | +| -------- | ----------------------------------------------------------------------------------------------------------- | ------------------------------------ | +| `path` | **Required.** The complete path to the file to upload. | - | +| `label` | Short description of the file displayed on the GitLab release. Used for the generic package file name. | File name extracted from the `path`. | +| `status` | Generic package status. Can be `default` and `hidden` (see official documents on [generic packages](https://docs.gitlab.com/ee/user/packages/generic_packages/)). | `default` | + +**Note**: If a file has a match in `generics` it will be included even if it also has a match in `.gitignore`. + +##### generics examples + +`'dist/app.js'`: include the `app.js` file in the `dist` directory. + +`[{path: 'dist/app.js', label: 'App'}]`: include the `dist/app.js` file and label it `App` in the generic package and the GitLab release. + ## 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 7d99d5fd..73e36dbd 100644 --- a/lib/definitions/errors.js +++ b/lib/definitions/errors.js @@ -12,6 +12,13 @@ module.exports = { 'README.md#assets' )}) must be an \`Array\` of \`Strings\` or \`Objects\` with a \`path\` property. Your configuration for the \`assets\` option is \`${stringify(assets)}\`.`, + }), + EINVALIDGENERICS: ({generics}) => ({ + message: 'Invalid `generics` option.', + details: `The [generics option](${linkify( + 'README.md#generics' + )}) must be an \`Array\` of \`Strings\` or \`Objects\` with a \`path\` property. +Your configuration for the \`generics\` option is \`${stringify(generics)}\`.`, }), EINVALIDGITLABURL: () => ({ message: 'The git repository URL is not a valid GitLab URL.', diff --git a/lib/get-file.js b/lib/get-file.js new file mode 100644 index 00000000..5532a14f --- /dev/null +++ b/lib/get-file.js @@ -0,0 +1,21 @@ +const {stat} = require('fs-extra'); +const {resolve} = require('path'); + +module.exports = async (path, {cwd, logger}) => { + const file = resolve(cwd, path); + let fileStat; + + try { + fileStat = await stat(file); + } catch (_) { + logger.error('The path %s cannot be read, and will be ignored.', path); + return null; + } + + if (!fileStat || !fileStat.isFile()) { + logger.error('The path %s is not a file, and will be ignored.', path); + return null; + } + + return file; +}; diff --git a/lib/publish.js b/lib/publish.js index d2384766..d2c31684 100644 --- a/lib/publish.js +++ b/lib/publish.js @@ -1,6 +1,5 @@ const {createReadStream} = require('fs'); -const {resolve} = require('path'); -const {stat} = require('fs-extra'); +const {basename} = require('path'); const {isPlainObject} = require('lodash'); const FormData = require('form-data'); const urlJoin = require('url-join'); @@ -8,16 +7,16 @@ const got = require('got'); const debug = require('debug')('semantic-release:gitlab'); const resolveConfig = require('./resolve-config'); const getRepoId = require('./get-repo-id'); +const getFile = require('./get-file'); const getAssets = require('./glob-assets'); module.exports = async (pluginConfig, context) => { const { - cwd, options: {repositoryUrl}, nextRelease: {gitTag, gitHead, notes}, logger, } = context; - const {gitlabToken, gitlabUrl, gitlabApiUrl, assets, milestones} = resolveConfig(pluginConfig, context); + const {gitlabToken, gitlabUrl, gitlabApiUrl, assets, generics, milestones} = resolveConfig(pluginConfig, context); const assetsList = []; const repoId = getRepoId(context, gitlabUrl, repositoryUrl); const encodedRepoId = encodeURIComponent(repoId); @@ -29,6 +28,53 @@ module.exports = async (pluginConfig, context) => { debug('release ref: %o', gitHead); debug('milestones: %o', milestones); + if (generics && generics.length > 0) { + debug('generics: %o', generics); + + await Promise.all( + generics.map(async generic => { + const {path, label, status} = isPlainObject(generic) + ? generic + : {path: generic, label: basename(generic), status: 'default'}; + const file = await getFile(path, context); + if (file === null) { + return; + } + + debug('file path: %o', path); + debug('file label: %o', label); + debug('file status: %o', status); + + // Upload generic package to the project + const form = new FormData(); + form.append('file', createReadStream(file)); + + const uploadEndpoint = urlJoin( + gitlabApiUrl, + `/projects/${encodedRepoId}/packages/generic/release/${encodedGitTag}/${label}${ + status ? `?status=${status}` : '' + }` + ); + + debug('PUT-ing the file %s to %s', file, uploadEndpoint); + + let response; + try { + response = await got.put(uploadEndpoint, {...apiOptions, body: form}).json(); + } catch (error) { + logger.error('An error occurred while uploading %s to the GitLab generics package API:\n%O', file, error); + throw error; + } + + const {url, alt} = response; + + assetsList.push({label, alt, url, type: 'package'}); + + logger.log('Uploaded file: %s', url); + }) + ); + } + if (assets && assets.length > 0) { const globbedAssets = await getAssets(context, assets); debug('globbed assets: %o', globbedAssets); @@ -36,18 +82,8 @@ module.exports = async (pluginConfig, context) => { await Promise.all( globbedAssets.map(async asset => { const {path, label, type, filepath} = isPlainObject(asset) ? asset : {path: asset}; - const file = resolve(cwd, path); - let fileStat; - - try { - fileStat = await stat(file); - } catch (_) { - logger.error('The asset %s cannot be read, and will be ignored.', path); - return; - } - - if (!fileStat || !fileStat.isFile()) { - logger.error('The asset %s is not a file, and will be ignored.', path); + const file = await getFile(path, context); + if (file === null) { return; } diff --git a/lib/resolve-config.js b/lib/resolve-config.js index 8fc3a413..c4c43e5f 100644 --- a/lib/resolve-config.js +++ b/lib/resolve-config.js @@ -2,7 +2,7 @@ const {castArray, isNil} = require('lodash'); const urlJoin = require('url-join'); module.exports = ( - {gitlabUrl, gitlabApiPathPrefix, assets, milestones}, + {gitlabUrl, gitlabApiPathPrefix, assets, generics, milestones}, { envCi: {service} = {}, env: { @@ -40,6 +40,7 @@ module.exports = ( ? CI_API_V4_URL : urlJoin(defaultedGitlabUrl, isNil(userGitlabApiPathPrefix) ? '/api/v4' : userGitlabApiPathPrefix), assets: assets ? castArray(assets) : assets, + generics: generics ? castArray(generics) : generics, milestones: milestones ? castArray(milestones) : milestones, }; }; diff --git a/lib/verify.js b/lib/verify.js index 1d6c8e5e..4eb809a6 100644 --- a/lib/verify.js +++ b/lib/verify.js @@ -15,6 +15,9 @@ const VALIDATORS = { assets: isArrayOf( asset => isStringOrStringArray(asset) || (isPlainObject(asset) && isStringOrStringArray(asset.path)) ), + generics: isArrayOf( + generic => isStringOrStringArray(generic) || (isPlainObject(generic) && isStringOrStringArray(generic.path)) + ), }; module.exports = async (pluginConfig, context) => { @@ -23,7 +26,7 @@ module.exports = async (pluginConfig, context) => { logger, } = context; const errors = []; - const {gitlabToken, gitlabUrl, gitlabApiUrl, assets} = resolveConfig(pluginConfig, context); + const {gitlabToken, gitlabUrl, gitlabApiUrl, assets, generics} = resolveConfig(pluginConfig, context); const repoId = getRepoId(context, gitlabUrl, repositoryUrl); debug('apiUrl: %o', gitlabApiUrl); debug('repoId: %o', repoId); @@ -36,6 +39,10 @@ module.exports = async (pluginConfig, context) => { errors.push(getError('EINVALIDASSETS')); } + if (generics && !VALIDATORS.generics(generics)) { + errors.push(getError('EINVALIDGENERICS')); + } + if (!gitlabToken) { errors.push(getError('ENOGLTOKEN', {repositoryUrl})); } diff --git a/test/publish.test.js b/test/publish.test.js index af1f795b..86423d10 100644 --- a/test/publish.test.js +++ b/test/publish.test.js @@ -83,6 +83,48 @@ test.serial('Publish a release with assets', async t => { t.true(gitlab.isDone()); }); +test.serial('Publish a release with generics', async t => { + const cwd = 'test/fixtures/files'; + const owner = 'test_user'; + const repo = 'test_repo'; + const env = {GITLAB_TOKEN: 'gitlab_token'}; + const nextRelease = {gitHead: '123', gitTag: 'v1.0.0', notes: 'Test release note body'}; + const options = {repositoryUrl: `https://gitlab.com/${owner}/${repo}.git`}; + const encodedRepoId = encodeURIComponent(`${owner}/${repo}`); + const encodedGitTag = encodeURIComponent(nextRelease.gitTag); + const uploaded = {url: '/uploads/file.css', alt: 'file.css'}; + const generics = ['file.css']; + const gitlab = authenticate(env) + .post(`/projects/${encodedRepoId}/releases`, { + tag_name: nextRelease.gitTag, + description: nextRelease.notes, + assets: { + links: [ + { + name: uploaded.alt, + url: `https://gitlab.com/${owner}/${repo}${uploaded.url}`, + link_type: 'package', + }, + ], + }, + }) + .reply(200); + const gitlabUpload = authenticate(env) + .put( + `/projects/${encodedRepoId}/packages/generic/release/${encodedGitTag}/file.css?status=default`, + /filename="file.css"/gm + ) + .reply(200, uploaded); + + const result = await publish({generics}, {env, cwd, options, nextRelease, logger: t.context.logger}); + + t.is(result.url, `https://gitlab.com/${encodedRepoId}/-/releases/${encodedGitTag}`); + t.deepEqual(t.context.log.args[0], ['Uploaded file: %s', uploaded.url]); + t.deepEqual(t.context.log.args[1], ['Published GitLab release: %s', nextRelease.gitTag]); + t.true(gitlabUpload.isDone()); + t.true(gitlab.isDone()); +}); + test.serial('Publish a release with asset type and permalink', async t => { const cwd = 'test/fixtures/files'; const owner = 'test_user'; @@ -183,6 +225,33 @@ test.serial('Publish a release with array of missing assets', async t => { t.true(gitlab.isDone()); }); +test.serial('Publish a release with array of missing generics', async t => { + const cwd = 'test/fixtures/files'; + const owner = 'test_user'; + const repo = 'test_repo'; + const env = {GITLAB_TOKEN: 'gitlab_token'}; + const nextRelease = {gitHead: '123', gitTag: 'v1.0.0', notes: 'Test release note body'}; + const options = {repositoryUrl: `https://gitlab.com/${owner}/${repo}.git`}; + const encodedRepoId = encodeURIComponent(`${owner}/${repo}`); + const encodedGitTag = encodeURIComponent(nextRelease.gitTag); + const emptyDirectory = tempy.directory(); + const generics = [emptyDirectory, {path: 'missing.txt', label: 'missing.txt'}]; + const gitlab = authenticate(env) + .post(`/projects/${encodedRepoId}/releases`, { + tag_name: nextRelease.gitTag, + description: nextRelease.notes, + assets: { + links: [], + }, + }) + .reply(200); + const result = await publish({generics}, {env, cwd, options, nextRelease, logger: t.context.logger}); + + t.is(result.url, `https://gitlab.com/${encodedRepoId}/-/releases/${encodedGitTag}`); + t.deepEqual(t.context.log.args[0], ['Published GitLab release: %s', nextRelease.gitTag]); + t.true(gitlab.isDone()); +}); + test.serial('Publish a release with one asset and custom label', async t => { const cwd = 'test/fixtures/files'; const owner = 'test_user'; diff --git a/test/resolve-config.test.js b/test/resolve-config.test.js index 3d0ddfd5..51152a6a 100644 --- a/test/resolve-config.test.js +++ b/test/resolve-config.test.js @@ -7,12 +7,14 @@ test('Returns user config', t => { const gitlabUrl = 'https://host.com'; const gitlabApiPathPrefix = '/api/prefix'; const assets = ['file.js']; + const generics = ['file.js']; - t.deepEqual(resolveConfig({gitlabUrl, gitlabApiPathPrefix, assets}, {env: {GITLAB_TOKEN: gitlabToken}}), { + t.deepEqual(resolveConfig({gitlabUrl, gitlabApiPathPrefix, assets, generics}, {env: {GITLAB_TOKEN: gitlabToken}}), { gitlabToken, gitlabUrl, gitlabApiUrl: urlJoin(gitlabUrl, gitlabApiPathPrefix), assets, + generics, milestones: undefined, }); }); @@ -22,11 +24,12 @@ test('Returns user config via environment variables', t => { const gitlabUrl = 'https://host.com'; const gitlabApiPathPrefix = '/api/prefix'; const assets = ['file.js']; + const generics = ['file.js']; const milestones = ['1.2.3']; t.deepEqual( resolveConfig( - {assets, milestones}, + {assets, generics, milestones}, {env: {GITLAB_TOKEN: gitlabToken, GITLAB_URL: gitlabUrl, GITLAB_PREFIX: gitlabApiPathPrefix}} ), { @@ -34,6 +37,7 @@ test('Returns user config via environment variables', t => { gitlabUrl, gitlabApiUrl: urlJoin(gitlabUrl, gitlabApiPathPrefix), assets, + generics, milestones, } ); @@ -44,14 +48,19 @@ test('Returns user config via alternative environment variables', t => { const gitlabUrl = 'https://host.com'; const gitlabApiPathPrefix = '/api/prefix'; const assets = ['file.js']; + const generics = ['file.js']; t.deepEqual( - resolveConfig({assets}, {env: {GL_TOKEN: gitlabToken, GL_URL: gitlabUrl, GL_PREFIX: gitlabApiPathPrefix}}), + resolveConfig( + {assets, generics}, + {env: {GL_TOKEN: gitlabToken, GL_URL: gitlabUrl, GL_PREFIX: gitlabApiPathPrefix}} + ), { gitlabToken, gitlabUrl, gitlabApiUrl: urlJoin(gitlabUrl, gitlabApiPathPrefix), assets, + generics, milestones: undefined, } ); @@ -67,6 +76,7 @@ test('Returns default config', t => { gitlabUrl: 'https://gitlab.com', gitlabApiUrl: urlJoin('https://gitlab.com', '/api/v4'), assets: undefined, + generics: undefined, milestones: undefined, }); @@ -75,6 +85,7 @@ test('Returns default config', t => { gitlabUrl: 'https://gitlab.com', gitlabApiUrl: urlJoin('https://gitlab.com', gitlabApiPathPrefix), assets: undefined, + generics: undefined, milestones: undefined, }); @@ -83,6 +94,7 @@ test('Returns default config', t => { gitlabUrl: 'https://gitlab.com', gitlabApiUrl: urlJoin(gitlabUrl, '/api/v4'), assets: undefined, + generics: undefined, milestones: undefined, }); }); @@ -106,6 +118,7 @@ test('Returns default config via GitLab CI/CD environment variables', t => { gitlabUrl: 'http://ci-host.com', gitlabApiUrl: CI_API_V4_URL, assets: undefined, + generics: undefined, milestones: undefined, } ); @@ -116,13 +129,14 @@ test('Returns user config over GitLab CI/CD environment variables', t => { const gitlabUrl = 'https://host.com'; const gitlabApiPathPrefix = '/api/prefix'; const assets = ['file.js']; + const generics = ['file.js']; const CI_PROJECT_URL = 'http://ci-host.com/ci-owner/ci-repo'; const CI_PROJECT_PATH = 'ci-owner/ci-repo'; const CI_API_V4_URL = 'http://ci-host-api.com/prefix'; t.deepEqual( resolveConfig( - {gitlabUrl, gitlabApiPathPrefix, assets}, + {gitlabUrl, gitlabApiPathPrefix, assets, generics}, { envCi: {service: 'gitlab'}, env: {GL_TOKEN: gitlabToken, CI_PROJECT_URL, CI_PROJECT_PATH, CI_API_V4_URL}, @@ -133,6 +147,7 @@ test('Returns user config over GitLab CI/CD environment variables', t => { gitlabUrl, gitlabApiUrl: urlJoin(gitlabUrl, gitlabApiPathPrefix), assets, + generics, milestones: undefined, } ); @@ -166,6 +181,7 @@ test('Returns user config via environment variables over GitLab CI/CD environmen gitlabUrl, gitlabApiUrl: urlJoin(gitlabUrl, gitlabApiPathPrefix), assets: undefined, + generics: undefined, milestones: undefined, } ); @@ -199,6 +215,7 @@ test('Returns user config via alternative environment variables over GitLab CI/C gitlabUrl, gitlabApiUrl: urlJoin(gitlabUrl, gitlabApiPathPrefix), assets: undefined, + generics: undefined, milestones: undefined, } ); @@ -223,6 +240,7 @@ test('Ignore GitLab CI/CD environment variables if not running on GitLab CI/CD', gitlabUrl: 'https://gitlab.com', gitlabApiUrl: urlJoin('https://gitlab.com', '/api/v4'), assets: undefined, + generics: undefined, milestones: undefined, } ); diff --git a/test/verify.test.js b/test/verify.test.js index 6e78dc59..d23eece7 100644 --- a/test/verify.test.js +++ b/test/verify.test.js @@ -398,6 +398,154 @@ test.serial( } ); +test.serial('Verify "generics" is a String', async t => { + const owner = 'test_user'; + const repo = 'test_repo'; + const env = {GITLAB_URL: 'https://othertesturl.com:443', GITLAB_TOKEN: 'gitlab_token', GITLAB_PREFIX: 'prefix'}; + const generics = 'file2.js'; + const gitlab = authenticate(env) + .get(`/projects/${owner}%2F${repo}`) + .reply(200, {permissions: {project_access: {access_level: 40}}}); + + await t.notThrowsAsync( + verify( + {generics}, + {env, options: {repositoryUrl: `git@othertesturl.com:${owner}/${repo}.git`}, logger: t.context.logger} + ) + ); + + t.true(gitlab.isDone()); +}); + +test.serial('Verify "generics" is an Object with a path property', async t => { + const owner = 'test_user'; + const repo = 'test_repo'; + const env = {GITLAB_URL: 'https://othertesturl.com:443', GITLAB_TOKEN: 'gitlab_token', GITLAB_PREFIX: 'prefix'}; + const generics = {path: 'file2.js'}; + const gitlab = authenticate(env) + .get(`/projects/${owner}%2F${repo}`) + .reply(200, {permissions: {project_access: {access_level: 40}}}); + + await t.notThrowsAsync( + verify( + {generics}, + {env, options: {repositoryUrl: `git@othertesturl.com:${owner}/${repo}.git`}, logger: t.context.logger} + ) + ); + + t.true(gitlab.isDone()); +}); + +test.serial('Verify "generics" is an Array of Object with a path property', async t => { + const owner = 'test_user'; + const repo = 'test_repo'; + const env = {GITLAB_URL: 'https://othertesturl.com:443', GITLAB_TOKEN: 'gitlab_token', GITLAB_PREFIX: 'prefix'}; + const generics = [{path: 'file1.js'}, {path: 'file2.js'}]; + const gitlab = authenticate(env) + .get(`/projects/${owner}%2F${repo}`) + .reply(200, {permissions: {project_access: {access_level: 40}}}); + + await t.notThrowsAsync( + verify( + {generics}, + {env, options: {repositoryUrl: `git@othertesturl.com:${owner}/${repo}.git`}, logger: t.context.logger} + ) + ); + + t.true(gitlab.isDone()); +}); + +test.serial('Throw SemanticReleaseError if "generics" option is not a String or an Array of Objects', async t => { + const owner = 'test_user'; + const repo = 'test_repo'; + const env = {GITLAB_TOKEN: 'gitlab_token'}; + const generics = 42; + const gitlab = authenticate(env) + .get(`/projects/${owner}%2F${repo}`) + .reply(200, {permissions: {project_access: {access_level: 40}}}); + + const [error, ...errors] = await t.throwsAsync( + verify( + {generics}, + {env, options: {repositoryUrl: `https://gitlab.com/${owner}/${repo}.git`}, logger: t.context.logger} + ) + ); + + t.is(errors.length, 0); + t.is(error.name, 'SemanticReleaseError'); + t.is(error.code, 'EINVALIDGENERICS'); + t.true(gitlab.isDone()); +}); + +test.serial('Throw SemanticReleaseError if "generics" option is an Array with invalid elements', async t => { + const owner = 'test_user'; + const repo = 'test_repo'; + const env = {GITLAB_TOKEN: 'gitlab_token'}; + const generics = ['file.js', 42]; + const gitlab = authenticate(env) + .get(`/projects/${owner}%2F${repo}`) + .reply(200, {permissions: {project_access: {access_level: 40}}}); + + const [error, ...errors] = await t.throwsAsync( + verify( + {generics}, + {env, options: {repositoryUrl: `https://gitlab.com/${owner}/${repo}.git`}, logger: t.context.logger} + ) + ); + + t.is(errors.length, 0); + t.is(error.name, 'SemanticReleaseError'); + t.is(error.code, 'EINVALIDGENERICS'); + t.true(gitlab.isDone()); +}); + +test.serial('Throw SemanticReleaseError if "generics" option is an Object missing the "path" property', async t => { + const owner = 'test_user'; + const repo = 'test_repo'; + const env = {GITLAB_TOKEN: 'gitlab_token'}; + const generics = {label: 'file'}; + const gitlab = authenticate(env) + .get(`/projects/${owner}%2F${repo}`) + .reply(200, {permissions: {project_access: {access_level: 40}}}); + + const [error, ...errors] = await t.throwsAsync( + verify( + {generics}, + {env, options: {repositoryUrl: `https://gitlab.com/${owner}/${repo}.git`}, logger: t.context.logger} + ) + ); + + t.is(errors.length, 0); + t.is(error.name, 'SemanticReleaseError'); + t.is(error.code, 'EINVALIDGENERICS'); + t.true(gitlab.isDone()); +}); + +test.serial( + 'Throw SemanticReleaseError if "generics" option is an Array with objects missing the "path" property', + async t => { + const owner = 'test_user'; + const repo = 'test_repo'; + const env = {GITLAB_TOKEN: 'gitlab_token'}; + const generics = [{path: 'lib/file.js'}, {label: 'file'}]; + const gitlab = authenticate(env) + .get(`/projects/${owner}%2F${repo}`) + .reply(200, {permissions: {project_access: {access_level: 40}}}); + + const [error, ...errors] = await t.throwsAsync( + verify( + {generics}, + {env, options: {repositoryUrl: `https://gitlab.com/${owner}/${repo}.git`}, logger: t.context.logger} + ) + ); + + t.is(errors.length, 0); + t.is(error.name, 'SemanticReleaseError'); + t.is(error.code, 'EINVALIDGENERICS'); + t.true(gitlab.isDone()); + } +); + test('Throw SemanticReleaseError for missing GitLab token', async t => { const env = {}; const [error, ...errors] = await t.throwsAsync(