Skip to content

Commit

Permalink
feat: Support globs for the assets option
Browse files Browse the repository at this point in the history
  • Loading branch information
pvdlg committed Dec 4, 2017
1 parent 0165914 commit 0e1b290
Show file tree
Hide file tree
Showing 11 changed files with 381 additions and 68 deletions.
93 changes: 55 additions & 38 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,78 +8,95 @@ Set of [semantic-release](https://github.com/semantic-release/semantic-release)

## verifyConditions

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

### Options
## publish

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

## publish
## Configuration

### Github Repository authentication

The `Github` authentication configuration is **required** and can be set via [environment variables](#environment-variables).

Only the [personal token](https://help.github.com/articles/creating-a-personal-access-token-for-the-command-line) authentication is supported.

Publish a [Github release](https://help.github.com/articles/about-releases).
### Environment variables

| Variable | Description |
| ------------------------------ | ----------------------------------------------------------|
| `GH_TOKEN` or `GITHUB_TOKEN` | **Required.** The token used to authenticate with GitHub. |
| `GH_URL` or `GITHUB_URL` | The GitHub Enterprise endpoint. |
| `GH_PREFIX` or `GITHUB_PREFIX` | The GitHub Enterprise API prefix. |

### Options

| Option | Description | Default |
| --------------------- | --------------------------------------------------------- | ------------------------------------------------------ |
| `githubToken` | **Required.** The token used to authenticate with GitHub. | `process.env.GH_TOKEN` or `process.env.GITHUB_TOKEN` |
| `githubUrl` | The GitHub Enterprise endpoint. | `process.env.GH_URL` or `process.env.GITHUB_URL` |
| `githubApiPathPrefix` | The GitHub Enterprise API prefix. | `process.env.GH_PREFIX` or `process.env.GITHUB_PREFIX` |
| `assets` | An array of files to upload to the release. | -
| Option | Description | Default |
| --------------------- | ------------------------------------------------------------------ | ---------------------------------------------------- |
| `githubToken` | **Required.** The token used to authenticate with GitHub. | `GH_TOKEN` or `GITHUB_TOKEN` environment variable. |
| `githubUrl` | The GitHub Enterprise endpoint. | `GH_URL` or `GITHUB_URL` environment variable. |
| `githubApiPathPrefix` | The GitHub Enterprise API prefix. | `GH_PREFIX` or `GITHUB_PREFIX` environment variable. |
| `assets` | An array of files to upload to the release. See [assets](#assets). | - |

#### assets option
#### `assets`

Each element of the array can be a path to the file or an `object` with the properties:
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

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

## Configuration
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"]`).

If a directory is configured, all the files under this directory and its children will be included.

Files can be included enven if they have a match in `.gitignore`.

##### `assets` examples

`'dist/*.js'`: include all the `js` files in the `dist` directory, but not in its sub-directories.

`[['dist', '!**/*.css']]`: include all the files in the `dist` directory and its sub-directories excluding the `css` files.

`[{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.

`[['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.

### Usage

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.

Each individual plugin can be disabled, replaced or used with other plugins in the `package.json`:

```json
{
"release": {
"verifyConditions": ["@semantic-release/github", "verify-other-condition"],
"getLastRelease": "custom-get-last-release",
"publish": [
"custom-publish",
{
"path": "@semantic-release/github",
"assets": [
{"path": "dist/asset.min.css", "label": "CSS distribution"},
{"path": "dist/asset.min.js", "label": "JS distribution"}
]
}
]
"verifyConditions": ["@semantic-release/github", "@semantic-release/npm", "verify-other-condition"],
"getLastRelease": "@semantic-release/npm",
"publish": ["@semantic-release/npm", "@semantic-release/github", "other-publish"]
}
}
```

The same configuration for Github Enterprise:
Options can be set within the plugin definition in the `semantic-release` configuration file:

```json
{
"release": {
"verifyConditions": [
"@semantic-release/npm",
{
"path": "@semantic-release/github",
"githubUrl": "https://my-ghe.com",
"githubApiPathPrefix": "/api-prefix"
},
"verify-other-condition"
],
"getLastRelease": "custom-get-last-release",
"publish": [
"custom-publish",
"@semantic-release/npm",
{
"path": "@semantic-release/github",
"githubUrl": "https://my-ghe.com",
Expand Down
49 changes: 49 additions & 0 deletions lib/glob-assets.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
const {basename} = require('path');
const {isPlainObject, castArray, uniqWith} = require('lodash');
const pReduce = require('p-reduce');
const globby = require('globby');
const debug = require('debug')('semantic-release:github');

module.exports = async assets =>
uniqWith(
(await pReduce(
assets,
async (result, asset) => {
// Wrap single glob definition in Array
const glob = castArray(isPlainObject(asset) ? asset.path : asset);
// Skip solo negated pattern (avoid to include every non js file with `!**/*.js`)
if (glob.length <= 1 && glob[0].startsWith('!')) {
debug(
'skipping the negated glob %o as its alone in its group and would retrieve a large amount of files ',
glob[0]
);
return result;
}
const globbed = await globby(glob, {expandDirectories: true, gitignore: false, dot: true});
if (isPlainObject(asset)) {
if (globbed.length > 1) {
// If asset is an Object with a glob the `path` property that resolve to multiple files,
// Output an Object definition for each file matched and set each one with:
// - `path` of the matched file
// - `name` based on the actual file name (to avoid assets with duplicate `name`)
// - other properties of the original asset definition
return [...result, ...globbed.map(file => Object.assign({}, asset, {path: file, name: basename(file)}))];
}
// If asset is an Object, output an Object definition with:
// - `path` of the matched file if there is one, or the original `path` definition (will be considered as a missing file)
// - other properties of the original asset definition
return [...result, Object.assign({}, asset, {path: globbed[0] || asset.path})];
}
if (globbed.length > 0) {
// If asset is a String definition, output each files matched
return [...result, ...globbed];
}
// 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)
return [...result, ...glob];
},
[]
// Sort with Object first, to prioritize Object definition over Strings in dedup
)).sort(asset => !isPlainObject(asset)),
// Compare `path` property if Object definition, value itself if String
(a, b) => (isPlainObject(a) ? a.path : a) === (isPlainObject(b) ? b.path : b)
);
23 changes: 16 additions & 7 deletions lib/publish.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
const {basename, extname} = require('path');
const {parse} = require('url');
const {stat, readFile} = require('fs-extra');
const {isPlainObject} = require('lodash');
const parseGithubUrl = require('parse-github-url');
const GitHubApi = require('github');
const pEachSeries = require('p-each-series');
const debug = require('debug')('semantic-release:publish-github');
const pReduce = require('p-reduce');
const mime = require('mime');
const debug = require('debug')('semantic-release:github');
const globAssets = require('./glob-assets.js');
const resolveConfig = require('./resolve-config');

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

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

if (assets && assets.length > 0) {
const globbedAssets = await globAssets(assets);
debug('globed assets: %o', globbedAssets);
// Make requests serially to avoid hitting the rate limit (https://developer.github.com/v3/guides/best-practices-for-integrators/#dealing-with-abuse-rate-limits)
await pEachSeries(assets, async asset => {
const filePath = typeof asset === 'object' ? asset.path : asset;
await pReduce(globbedAssets, async (_, asset) => {
const filePath = isPlainObject(asset) ? asset.path : asset;
let file;

try {
file = await stat(filePath);
} catch (err) {
Expand All @@ -54,19 +60,22 @@ module.exports = async (pluginConfig, {branch, repositoryUrl}, {version, gitHead
logger.error('The asset %s is not a file, and will be ignored.', filePath);
return;
}

const fileName = asset.name || basename(filePath);
const upload = {
owner,
repo,
url: uploadUrl,
file: await readFile(filePath),
contentType: mime.getType(extname(fileName)),
contentType: mime.getType(extname(fileName)) || 'text/plain',
contentLength: file.size,
name: fileName,
};

debug('file path: %o', filePath);
debug('file name: %o', fileName);
if (asset.label) {

if (isPlainObject(asset) && asset.label) {
upload.label = asset.label;
}

Expand Down
4 changes: 3 additions & 1 deletion lib/resolve-config.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
const {castArray} = require('lodash');

module.exports = ({githubToken, githubUrl, githubApiPathPrefix, assets}) => ({
githubToken: githubToken || process.env.GH_TOKEN || process.env.GITHUB_TOKEN,
githubUrl: githubUrl || process.env.GH_URL || process.env.GITHUB_URL,
githubApiPathPrefix: githubApiPathPrefix || process.env.GH_PREFIX || process.env.GITHUB_PREFIX,
assets: assets ? (Array.isArray(assets) ? assets : [assets]) : assets,
assets: assets ? castArray(assets) : assets,
});
25 changes: 17 additions & 8 deletions lib/verify.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
const {parse} = require('url');
const {isString, isPlainObject, isUndefined, isArray} = require('lodash');
const parseGithubUrl = require('parse-github-url');
const GitHubApi = require('github');
const SemanticReleaseError = require('@semantic-release/error');
Expand All @@ -11,14 +12,18 @@ module.exports = async (pluginConfig, {repositoryUrl}) => {
throw new SemanticReleaseError('No github token specified.', 'ENOGHTOKEN');
}

if (assets && assets.length > 0) {
// Verify that every asset is either a string or an object with path attribute defined
if (!assets.every(asset => typeof asset === 'string' || (typeof asset === 'object' && Boolean(asset.path)))) {
throw new SemanticReleaseError(
'The "assets" options must be an Array of strings or objects with a path property.',
'EINVALIDASSETS'
);
}
if (
!isUndefined(assets) &&
assets !== false &&
!(
isArray(assets) &&
assets.every(asset => isStringOrStringArray(asset) || (isPlainObject(asset) && isStringOrStringArray(asset.path)))
)
) {
throw new SemanticReleaseError(
'The "assets" options must be an Array of Strings or Objects with a path property.',
'EINVALIDASSETS'
);
}

const {name: repo, owner} = parseGithubUrl(repositoryUrl);
Expand Down Expand Up @@ -53,3 +58,7 @@ module.exports = async (pluginConfig, {repositoryUrl}) => {
);
}
};

function isStringOrStringArray(value) {
return isString(value) || (isArray(value) && value.every(isString));
}
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,10 @@
"debug": "^3.1.0",
"fs-extra": "^4.0.2",
"github": "^13.0.0",
"globby": "^7.1.1",
"lodash": "^4.17.4",
"mime": "^2.0.3",
"p-each-series": "^1.0.0",
"p-reduce": "^1.0.0",
"parse-github-url": "^1.0.1"
},
"devDependencies": {
Expand Down
1 change: 1 addition & 0 deletions test/fixtures/.dotfile
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Upload file content
Loading

0 comments on commit 0e1b290

Please sign in to comment.