Skip to content
Closed
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
21 changes: 21 additions & 0 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
@@ -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"
}
]
}
22 changes: 22 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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"}
]
}],
]
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down
7 changes: 7 additions & 0 deletions lib/definitions/errors.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.',
Expand Down
21 changes: 21 additions & 0 deletions lib/get-file.js
Original file line number Diff line number Diff line change
@@ -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;
};
68 changes: 52 additions & 16 deletions lib/publish.js
Original file line number Diff line number Diff line change
@@ -1,23 +1,22 @@
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');
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);
Expand All @@ -29,25 +28,62 @@ 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);

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;
}

Expand Down
3 changes: 2 additions & 1 deletion lib/resolve-config.js
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down Expand Up @@ -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,
};
};
9 changes: 8 additions & 1 deletion lib/verify.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand All @@ -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);
Expand All @@ -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}));
}
Expand Down
69 changes: 69 additions & 0 deletions test/publish.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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';
Expand Down
Loading