Skip to content

Commit 0e1b290

Browse files
committed
feat: Support globs for the assets option
1 parent 0165914 commit 0e1b290

File tree

11 files changed

+381
-68
lines changed

11 files changed

+381
-68
lines changed

README.md

Lines changed: 55 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -8,78 +8,95 @@ Set of [semantic-release](https://github.com/semantic-release/semantic-release)
88

99
## verifyConditions
1010

11-
Verify the presence and the validity of the `githubToken` (set via option or environment variable).
11+
Verify the presence and the validity of the `githubToken` (set via option or environment variable) and the `assets` option configuration.
1212

13-
### Options
13+
## publish
1414

15-
| Option | Description | Default |
16-
| --------------------- | --------------------------------------------------------- | ------------------------------------------------------ |
17-
| `githubToken` | **Required.** The token used to authenticate with GitHub. | `process.env.GH_TOKEN` or `process.env.GITHUB_TOKEN` |
18-
| `githubUrl` | The GitHub Enterprise endpoint. | `process.env.GH_URL` or `process.env.GITHUB_URL` |
19-
| `githubApiPathPrefix` | The GitHub Enterprise API prefix. | `process.env.GH_PREFIX` or `process.env.GITHUB_PREFIX` |
15+
Publish a [Github release](https://help.github.com/articles/about-releases), optionnaly uploading files.
2016

21-
## publish
17+
## Configuration
18+
19+
### Github Repository authentication
20+
21+
The `Github` authentication configuration is **required** and can be set via [environment variables](#environment-variables).
22+
23+
Only the [personal token](https://help.github.com/articles/creating-a-personal-access-token-for-the-command-line) authentication is supported.
2224

23-
Publish a [Github release](https://help.github.com/articles/about-releases).
25+
### Environment variables
26+
27+
| Variable | Description |
28+
| ------------------------------ | ----------------------------------------------------------|
29+
| `GH_TOKEN` or `GITHUB_TOKEN` | **Required.** The token used to authenticate with GitHub. |
30+
| `GH_URL` or `GITHUB_URL` | The GitHub Enterprise endpoint. |
31+
| `GH_PREFIX` or `GITHUB_PREFIX` | The GitHub Enterprise API prefix. |
2432

2533
### Options
2634

27-
| Option | Description | Default |
28-
| --------------------- | --------------------------------------------------------- | ------------------------------------------------------ |
29-
| `githubToken` | **Required.** The token used to authenticate with GitHub. | `process.env.GH_TOKEN` or `process.env.GITHUB_TOKEN` |
30-
| `githubUrl` | The GitHub Enterprise endpoint. | `process.env.GH_URL` or `process.env.GITHUB_URL` |
31-
| `githubApiPathPrefix` | The GitHub Enterprise API prefix. | `process.env.GH_PREFIX` or `process.env.GITHUB_PREFIX` |
32-
| `assets` | An array of files to upload to the release. | -
35+
| Option | Description | Default |
36+
| --------------------- | ------------------------------------------------------------------ | ---------------------------------------------------- |
37+
| `githubToken` | **Required.** The token used to authenticate with GitHub. | `GH_TOKEN` or `GITHUB_TOKEN` environment variable. |
38+
| `githubUrl` | The GitHub Enterprise endpoint. | `GH_URL` or `GITHUB_URL` environment variable. |
39+
| `githubApiPathPrefix` | The GitHub Enterprise API prefix. | `GH_PREFIX` or `GITHUB_PREFIX` environment variable. |
40+
| `assets` | An array of files to upload to the release. See [assets](#assets). | - |
3341

34-
#### assets option
42+
#### `assets`
3543

36-
Each element of the array can be a path to the file or an `object` with the properties:
44+
Can be a [glob](https://github.com/isaacs/node-glob#glob-primer) or and `Array` of [globs](https://github.com/isaacs/node-glob#glob-primer) and `Object`s with the following properties
3745

38-
| Property | Description | Default |
39-
| -------- | ------------------------------------------------------------------------ | ------------------------------------ |
40-
| `path` | **Required.** The file path to upload relative to the project directory. | - |
41-
| `name` | The name of the downloadable file on the Github release. | File name extracted from the `path`. |
42-
| `label` | Short description of the file displayed on the Github release. | - |
46+
| Property | Description | Default |
47+
| -------- | -------------------------------------------------------------------------------------------------------- | ------------------------------------ |
48+
| `path` | **Required.** A [glob](https://github.com/isaacs/node-glob#glob-primer) to identify the files to upload. | - |
49+
| `name` | The name of the downloadable file on the Github release. | File name extracted from the `path`. |
50+
| `label` | Short description of the file displayed on the Github release. | - |
4351

44-
## Configuration
52+
Each entry in the `assets` `Array` is globbed individually. A [glob](https://github.com/isaacs/node-glob#glob-primer) can be a `String` (`"dist/**/*.js"` or `"dist/mylib.js"`) or an `Array` of `String`s that will be globbed together (`["dist/**", "!**/*.css"]`).
53+
54+
If a directory is configured, all the files under this directory and its children will be included.
55+
56+
Files can be included enven if they have a match in `.gitignore`.
57+
58+
##### `assets` examples
59+
60+
`'dist/*.js'`: include all the `js` files in the `dist` directory, but not in its sub-directories.
61+
62+
`[['dist', '!**/*.css']]`: include all the files in the `dist` directory and its sub-directories excluding the `css` files.
63+
64+
`[{path: 'dist/MyLibrary.js', label: 'MyLibrary JS distribution'}, {path: 'dist/MyLibrary.css', label: 'MyLibrary CSS distribution'}]`: include the `dist/MyLibrary.js` and `dist/MyLibrary.css` files, and label them `MyLibrary JS distribution` and `MyLibrary CSS distribution` in the Github release.
65+
66+
`[['dist/**/*.{js,css}', '!**/*.min.*'], {path: 'build/MyLibrary.zip', label: 'MyLibrary'}]`: include all the `js` and `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 Github release.
67+
68+
### Usage
4569

4670
The plugins are used by default by [semantic-release](https://github.com/semantic-release/semantic-release) so no specific configuration is requiered if `githubToken`, `githubUrl` and `githubApiPathPrefix` are set via environment variable.
4771

4872
Each individual plugin can be disabled, replaced or used with other plugins in the `package.json`:
73+
4974
```json
5075
{
5176
"release": {
52-
"verifyConditions": ["@semantic-release/github", "verify-other-condition"],
53-
"getLastRelease": "custom-get-last-release",
54-
"publish": [
55-
"custom-publish",
56-
{
57-
"path": "@semantic-release/github",
58-
"assets": [
59-
{"path": "dist/asset.min.css", "label": "CSS distribution"},
60-
{"path": "dist/asset.min.js", "label": "JS distribution"}
61-
]
62-
}
63-
]
77+
"verifyConditions": ["@semantic-release/github", "@semantic-release/npm", "verify-other-condition"],
78+
"getLastRelease": "@semantic-release/npm",
79+
"publish": ["@semantic-release/npm", "@semantic-release/github", "other-publish"]
6480
}
6581
}
6682
```
6783

68-
The same configuration for Github Enterprise:
84+
Options can be set within the plugin definition in the `semantic-release` configuration file:
85+
6986
```json
7087
{
7188
"release": {
7289
"verifyConditions": [
90+
"@semantic-release/npm",
7391
{
7492
"path": "@semantic-release/github",
7593
"githubUrl": "https://my-ghe.com",
7694
"githubApiPathPrefix": "/api-prefix"
7795
},
7896
"verify-other-condition"
7997
],
80-
"getLastRelease": "custom-get-last-release",
8198
"publish": [
82-
"custom-publish",
99+
"@semantic-release/npm",
83100
{
84101
"path": "@semantic-release/github",
85102
"githubUrl": "https://my-ghe.com",

lib/glob-assets.js

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
const {basename} = require('path');
2+
const {isPlainObject, castArray, uniqWith} = require('lodash');
3+
const pReduce = require('p-reduce');
4+
const globby = require('globby');
5+
const debug = require('debug')('semantic-release:github');
6+
7+
module.exports = async assets =>
8+
uniqWith(
9+
(await pReduce(
10+
assets,
11+
async (result, asset) => {
12+
// Wrap single glob definition in Array
13+
const glob = castArray(isPlainObject(asset) ? asset.path : asset);
14+
// Skip solo negated pattern (avoid to include every non js file with `!**/*.js`)
15+
if (glob.length <= 1 && glob[0].startsWith('!')) {
16+
debug(
17+
'skipping the negated glob %o as its alone in its group and would retrieve a large amount of files ',
18+
glob[0]
19+
);
20+
return result;
21+
}
22+
const globbed = await globby(glob, {expandDirectories: true, gitignore: false, dot: true});
23+
if (isPlainObject(asset)) {
24+
if (globbed.length > 1) {
25+
// If asset is an Object with a glob the `path` property that resolve to multiple files,
26+
// Output an Object definition for each file matched and set each one with:
27+
// - `path` of the matched file
28+
// - `name` based on the actual file name (to avoid assets with duplicate `name`)
29+
// - other properties of the original asset definition
30+
return [...result, ...globbed.map(file => Object.assign({}, asset, {path: file, name: basename(file)}))];
31+
}
32+
// If asset is an Object, output an Object definition with:
33+
// - `path` of the matched file if there is one, or the original `path` definition (will be considered as a missing file)
34+
// - other properties of the original asset definition
35+
return [...result, Object.assign({}, asset, {path: globbed[0] || asset.path})];
36+
}
37+
if (globbed.length > 0) {
38+
// If asset is a String definition, output each files matched
39+
return [...result, ...globbed];
40+
}
41+
// If asset is a String definition but no match is found, output the elements of the original glob (each one will be considered as a missing file)
42+
return [...result, ...glob];
43+
},
44+
[]
45+
// Sort with Object first, to prioritize Object definition over Strings in dedup
46+
)).sort(asset => !isPlainObject(asset)),
47+
// Compare `path` property if Object definition, value itself if String
48+
(a, b) => (isPlainObject(a) ? a.path : a) === (isPlainObject(b) ? b.path : b)
49+
);

lib/publish.js

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
const {basename, extname} = require('path');
22
const {parse} = require('url');
33
const {stat, readFile} = require('fs-extra');
4+
const {isPlainObject} = require('lodash');
45
const parseGithubUrl = require('parse-github-url');
56
const GitHubApi = require('github');
6-
const pEachSeries = require('p-each-series');
7-
const debug = require('debug')('semantic-release:publish-github');
7+
const pReduce = require('p-reduce');
88
const mime = require('mime');
9+
const debug = require('debug')('semantic-release:github');
10+
const globAssets = require('./glob-assets.js');
911
const resolveConfig = require('./resolve-config');
1012

1113
module.exports = async (pluginConfig, {branch, repositoryUrl}, {version, gitHead, gitTag, notes}, logger) => {
@@ -27,23 +29,27 @@ module.exports = async (pluginConfig, {branch, repositoryUrl}, {version, gitHead
2729
try {
2830
// Test if the tag already exists
2931
await github.gitdata.getReference({owner, repo, ref: `tags/${gitTag}`});
32+
debug('The git tag %o already exists', gitTag);
3033
} catch (err) {
3134
// If the error is 404, the tag doesn't exist, otherwise it's an error
3235
if (err.code !== 404) {
3336
throw err;
3437
}
35-
debug('Create git tag %o with commit %o', ref, gitHead);
38+
debug('Create git tag %o with commit %o', gitTag, gitHead);
3639
await github.gitdata.createReference({owner, repo, ref, sha: gitHead});
3740
}
3841

3942
const {data: {html_url: htmlUrl, upload_url: uploadUrl}} = await github.repos.createRelease(release);
4043
logger.log('Published Github release: %s', htmlUrl);
4144

4245
if (assets && assets.length > 0) {
46+
const globbedAssets = await globAssets(assets);
47+
debug('globed assets: %o', globbedAssets);
4348
// Make requests serially to avoid hitting the rate limit (https://developer.github.com/v3/guides/best-practices-for-integrators/#dealing-with-abuse-rate-limits)
44-
await pEachSeries(assets, async asset => {
45-
const filePath = typeof asset === 'object' ? asset.path : asset;
49+
await pReduce(globbedAssets, async (_, asset) => {
50+
const filePath = isPlainObject(asset) ? asset.path : asset;
4651
let file;
52+
4753
try {
4854
file = await stat(filePath);
4955
} catch (err) {
@@ -54,19 +60,22 @@ module.exports = async (pluginConfig, {branch, repositoryUrl}, {version, gitHead
5460
logger.error('The asset %s is not a file, and will be ignored.', filePath);
5561
return;
5662
}
63+
5764
const fileName = asset.name || basename(filePath);
5865
const upload = {
5966
owner,
6067
repo,
6168
url: uploadUrl,
6269
file: await readFile(filePath),
63-
contentType: mime.getType(extname(fileName)),
70+
contentType: mime.getType(extname(fileName)) || 'text/plain',
6471
contentLength: file.size,
6572
name: fileName,
6673
};
74+
6775
debug('file path: %o', filePath);
6876
debug('file name: %o', fileName);
69-
if (asset.label) {
77+
78+
if (isPlainObject(asset) && asset.label) {
7079
upload.label = asset.label;
7180
}
7281

lib/resolve-config.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1+
const {castArray} = require('lodash');
2+
13
module.exports = ({githubToken, githubUrl, githubApiPathPrefix, assets}) => ({
24
githubToken: githubToken || process.env.GH_TOKEN || process.env.GITHUB_TOKEN,
35
githubUrl: githubUrl || process.env.GH_URL || process.env.GITHUB_URL,
46
githubApiPathPrefix: githubApiPathPrefix || process.env.GH_PREFIX || process.env.GITHUB_PREFIX,
5-
assets: assets ? (Array.isArray(assets) ? assets : [assets]) : assets,
7+
assets: assets ? castArray(assets) : assets,
68
});

lib/verify.js

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
const {parse} = require('url');
2+
const {isString, isPlainObject, isUndefined, isArray} = require('lodash');
23
const parseGithubUrl = require('parse-github-url');
34
const GitHubApi = require('github');
45
const SemanticReleaseError = require('@semantic-release/error');
@@ -11,14 +12,18 @@ module.exports = async (pluginConfig, {repositoryUrl}) => {
1112
throw new SemanticReleaseError('No github token specified.', 'ENOGHTOKEN');
1213
}
1314

14-
if (assets && assets.length > 0) {
15-
// Verify that every asset is either a string or an object with path attribute defined
16-
if (!assets.every(asset => typeof asset === 'string' || (typeof asset === 'object' && Boolean(asset.path)))) {
17-
throw new SemanticReleaseError(
18-
'The "assets" options must be an Array of strings or objects with a path property.',
19-
'EINVALIDASSETS'
20-
);
21-
}
15+
if (
16+
!isUndefined(assets) &&
17+
assets !== false &&
18+
!(
19+
isArray(assets) &&
20+
assets.every(asset => isStringOrStringArray(asset) || (isPlainObject(asset) && isStringOrStringArray(asset.path)))
21+
)
22+
) {
23+
throw new SemanticReleaseError(
24+
'The "assets" options must be an Array of Strings or Objects with a path property.',
25+
'EINVALIDASSETS'
26+
);
2227
}
2328

2429
const {name: repo, owner} = parseGithubUrl(repositoryUrl);
@@ -53,3 +58,7 @@ module.exports = async (pluginConfig, {repositoryUrl}) => {
5358
);
5459
}
5560
};
61+
62+
function isStringOrStringArray(value) {
63+
return isString(value) || (isArray(value) && value.every(isString));
64+
}

package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,10 @@
2020
"debug": "^3.1.0",
2121
"fs-extra": "^4.0.2",
2222
"github": "^13.0.0",
23+
"globby": "^7.1.1",
24+
"lodash": "^4.17.4",
2325
"mime": "^2.0.3",
24-
"p-each-series": "^1.0.0",
26+
"p-reduce": "^1.0.0",
2527
"parse-github-url": "^1.0.1"
2628
},
2729
"devDependencies": {

test/fixtures/.dotfile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Upload file content

0 commit comments

Comments
 (0)