Skip to content

Commit

Permalink
feat: allow adding URLs to release assets (#322)
Browse files Browse the repository at this point in the history
Co-authored-by: Florian Greinacher <florian@greinacher.de>

Fixes #154
  • Loading branch information
awcjack committed May 16, 2022
1 parent a8d1ce5 commit 3a9fb9a
Show file tree
Hide file tree
Showing 3 changed files with 91 additions and 36 deletions.
4 changes: 3 additions & 1 deletion README.md
Expand Up @@ -35,6 +35,7 @@ The plugin can be configured in the [**semantic-release** configuration file](ht
{"path": "dist/asset.min.css", "label": "CSS distribution"},
{"path": "dist/asset.min.js", "label": "JS distribution"},
{"path": "dist/asset.min.js", "label": "v${nextRelease.version}.js"}
{"url": "https://gitlab.com/gitlab-org/gitlab/-/blob/master/README.md"}
]
}],
]
Expand Down Expand Up @@ -95,7 +96,8 @@ Can be a [glob](https://github.com/isaacs/node-glob#glob-primer) or and `Array`

| Property | Description | Default |
| -------- | ----------------------------------------------------------------------------------------------------------- | ------------------------------------ |
| `path` | **Required.** A [glob](https://github.com/isaacs/node-glob#glob-primer) to identify the files to upload. | - |
| `path` | **Required**, unless `url` is set. A [glob](https://github.com/isaacs/node-glob#glob-primer) to identify the files to upload. | - |
| `url` | Alternative to setting `path` this provides the ability to add links to releases, e.g. URLs to container images. | - |
| `label` | Short description of the file displayed on the GitLab release. Can be dynamically adjusted with [Lodash template](https://lodash.com/docs#template). Allows same variables as [`successComment`](#successComment). Ignored if `path` matches more than one file. | File name extracted from the `path`. |
| `type` | Asset type displayed on the GitLab release. Can be `runbook`, `package`, `image` and `other` (see official documents on [release assets](https://docs.gitlab.com/ee/user/project/releases/#release-assets)). | `other` |
| `filepath` | A filepath for creating a permalink pointing to the asset (requires GitLab 12.9+, see official documents on [permanent links](https://docs.gitlab.com/ee/user/project/releases/#permanent-links-to-release-assets)). Ignored if `path` matches more than one file. | - |
Expand Down
85 changes: 50 additions & 35 deletions lib/publish.js
Expand Up @@ -31,54 +31,69 @@ module.exports = async (pluginConfig, context) => {
debug('milestones: %o', milestones);

if (assets && assets.length > 0) {
const globbedAssets = await getAssets(context, assets);
// Skip glob if url is provided
const urlAssets = assets.filter((asset) => asset.url);
debug('url assets: %o', urlAssets);
const globbedAssets = await getAssets(
context,
assets.filter((asset) => !asset.url)
);
debug('globbed assets: %o', globbedAssets);
const allAssets = [...urlAssets, ...globbedAssets];
debug('all assets: %o', allAssets);

await Promise.all(
globbedAssets.map(async (asset) => {
allAssets.map(async (asset) => {
const {path, type, filepath} = isPlainObject(asset) ? asset : {path: asset};
const file = pathlib.resolve(cwd, path);
const label = asset.label ? template(asset.label)(context) : undefined;
let fileStat;
const _url = asset.url;
if (_url) {
assetsList.push({label, rawUrl: _url, type, filepath});
debug('use link from release setting: %s', _url);
} else {
const file = pathlib.resolve(cwd, path);

try {
fileStat = await stat(file);
} catch {
logger.error('The asset %s cannot be read, and will be ignored.', path);
return;
}
let fileStat;

if (!fileStat || !fileStat.isFile()) {
logger.error('The asset %s is not a file, and will be ignored.', path);
return;
}
try {
fileStat = await stat(file);
} catch {
logger.error('The asset %s cannot be read, and will be ignored.', path);
return;
}

debug('file path: %o', path);
debug('file label: %o', label);
debug('file type: %o', type);
debug('file filepath: %o', filepath);
if (!fileStat || !fileStat.isFile()) {
logger.error('The asset %s is not a file, and will be ignored.', path);
return;
}

// Uploaded assets to the project
const form = new FormData();
form.append('file', createReadStream(file));
debug('file path: %o', path);
debug('file label: %o', label);
debug('file type: %o', type);
debug('file filepath: %o', filepath);

const uploadEndpoint = urlJoin(gitlabApiUrl, `/projects/${encodedRepoId}/uploads`);
// Uploaded assets to the project
const form = new FormData();
form.append('file', createReadStream(file));

debug('POST-ing the file %s to %s', file, uploadEndpoint);
const uploadEndpoint = urlJoin(gitlabApiUrl, `/projects/${encodedRepoId}/uploads`);

let response;
try {
response = await got.post(uploadEndpoint, {...apiOptions, ...proxy, body: form}).json();
} catch (error) {
logger.error('An error occurred while uploading %s to the GitLab project uploads API:\n%O', file, error);
throw error;
}
debug('POST-ing the file %s to %s', file, uploadEndpoint);

let response;
try {
response = await got.post(uploadEndpoint, {...apiOptions, ...proxy, body: form}).json();
} catch (error) {
logger.error('An error occurred while uploading %s to the GitLab project uploads API:\n%O', file, error);
throw error;
}

const {url, alt} = response;
const {url, alt} = response;

assetsList.push({label, alt, url, type, filepath});
assetsList.push({label, alt, url, type, filepath});

logger.log('Uploaded file: %s', url);
logger.log('Uploaded file: %s', url);
}
})
);
}
Expand All @@ -93,10 +108,10 @@ module.exports = async (pluginConfig, context) => {
description: notes && notes.trim() ? notes : gitTag,
milestones,
assets: {
links: assetsList.map(({label, alt, url, type, filepath}) => {
links: assetsList.map(({label, alt, url, type, filepath, rawUrl}) => {
return {
name: label || alt,
url: urlJoin(gitlabUrl, repoId, url),
url: rawUrl || urlJoin(gitlabUrl, repoId, url),
link_type: type,
filepath,
};
Expand Down
38 changes: 38 additions & 0 deletions test/publish.test.js
Expand Up @@ -294,3 +294,41 @@ test.serial('Publish a release with missing release notes', async (t) => {
t.deepEqual(t.context.log.args[0], ['Published GitLab release: %s', nextRelease.gitTag]);
t.true(gitlab.isDone());
});

test.serial('Publish a release with an asset link', 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 link = {
label: 'README.md',
type: 'other',
url: 'https://gitlab.com/gitlab-org/gitlab/-/blob/master/README.md',
};
const assets = [link];
const gitlab = authenticate(env)
.post(`/projects/${encodedRepoId}/releases`, {
tag_name: nextRelease.gitTag,
description: nextRelease.notes,
assets: {
links: [
{
name: 'README.md',
url: `https://gitlab.com/gitlab-org/gitlab/-/blob/master/README.md`,
link_type: 'other',
},
],
},
})
.reply(200);

const result = await publish({assets}, {env, cwd, 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.true(gitlab.isDone());
});

0 comments on commit 3a9fb9a

Please sign in to comment.