Compare commits

..
53 Commits
Author SHA1 Message Date
Stephan Bönnemann ddaec4f2bf test(integration): make em at least run locally 2015-07-19 16:21:40 +02:00
Stephan Bönnemann 22a283a027 chore: prepare to move back to main repo#next 2015-07-19 16:05:03 +02:00
Stephan Bönnemann d9eeb3fcae refactor: use one config object instead of passing arguments to plugins on their own
This commit does a lot but it's all connected and tries to make everything more extensible and future proof.

1. CLI arguments and options from the "package.json" are no longer treated as two different things. You can now pass options either way.

BREAKING CHANGE: cli arguments are now normalized to camelCase, so e.g. `options['github-url']` is now `options.githubUrl`

2. Plugins no longer receive config they need one by one, but in one huge object. This way it's easier to pass more info in the future w/o breaking existing plugins that rely on the position of the callback in the arguments array.

BREAKING CHANGE: Plugins now need to read their passed options from one huge config object.

Old:
```js
module.exports = function (pluginConfig, foo, bar, cb) {…}
```

New:
```js
// ES5
module.exports = function(pluginConfig, config, cb) {
  var foo = config.foo
  var bar = config.bar
  …
}

// ES6
module.exports = function (pluginConfig, {foo, bar}, cb) {…}
```
2015-07-19 15:34:30 +02:00
Stephan Bönnemann 8892ec7f7a fix(plugins): typo in plugin name 2015-07-17 16:51:06 +02:00
Stephan Bönnemann cf27cd16cc chore(package): ignore config files 2015-07-17 15:10:10 +02:00
Stephan Bönnemann 152d1f6874 chore(travis): simplify config 2015-07-17 14:56:56 +02:00
Stephan Bönnemann 626315e9fe feat(package): update condition-travis 2015-07-17 14:56:44 +02:00
Stephan Bönnemann f5573f7494 chore(package): bump condition-travis 2015-07-17 10:01:52 +02:00
Stephan Bönnemann 2d3506cda1 chore: setup semantic-release 2015-07-17 09:35:53 +02:00
Stephan Bönnemann 72f9f8ea7c chore: rename to scripts 2015-07-16 19:36:42 +02:00
Stephan Bönnemann 59d852d6fb feat(post): initial 2015-07-16 17:30:23 +02:00
Stephan Bönnemann ed05101a61 test(post): initial 2015-07-16 17:30:14 +02:00
Stephan Bönnemann 431babb42e fix(verify): ensure repo url in package.json is well formed 2015-07-16 14:07:52 +02:00
Stephan Bönnemann dbea5cc1b1 feat(last-release): add custom dist-tag 2015-07-16 11:51:33 +02:00
Stephan Bönnemann 6cf1711151 test(last-release): add custom dist-tag 2015-07-16 11:51:22 +02:00
Stephan Bönnemann ba5222af52 chore(package): update semver 2015-07-14 21:13:36 +02:00
Stephan Bönnemann b1d9985ba3 test(plugins): test exported plugins 2015-07-14 21:13:36 +02:00
Stephan Bönnemann 13e2f5ce78 feat(cli): add verifyConditions hook 2015-07-14 21:13:36 +02:00
Stephan Bönnemann af15fb4c61 fix(last-release): normalized registry url 2015-07-14 21:13:36 +02:00
Stephan Bönnemann caf09c9be0 fix(cli): only write npmrc when necessary 2015-07-14 18:30:30 +02:00
Stephan Bönnemann 0316babf7a refactor(error): use error module 2015-07-14 12:25:36 +02:00
Stephan Bönnemann 43c8df0bfe fix(plugins): require user plugins relative to the cwd 2015-07-13 12:03:05 +02:00
Stephan Bönnemann 150273c8fe fix(plugins): rename plugins 2015-07-13 11:53:32 +02:00
Stephan Bönnemann f247ed8741 chore(package): remove integration tests from default test command 2015-07-09 15:33:59 +02:00
Stephan Bönnemann 2eff4a4467 style(type): add dot at end of sentence 2015-07-09 02:10:39 +02:00
Stephan Bönnemann e1d5da5864 fix(plugins): pass right path to generateNotes 2015-07-09 02:10:39 +02:00
Stephan Bönnemann a0190b7ad3 feat(bin): implement cli 2015-07-09 02:10:39 +02:00
Stephan Bönnemann c6f95e4870 test(pre): initial cli/integration suite 2015-07-09 02:10:39 +02:00
Stephan Bönnemann 6d84b66c05 test(test-module): creates integration test env on disk 2015-07-09 02:10:38 +02:00
Stephan Bönnemann 4d58af7145 test(base-scenario): creates nixt testing base 2015-07-09 02:10:38 +02:00
Stephan Bönnemann 9a3d9a9280 chore: add editorconfig 2015-07-09 02:10:38 +02:00
Stephan Bönnemann 32c1dcf75b test(registry): lib to start and stop an npm-registry-couchapp 2015-07-09 02:10:38 +02:00
Stephan Bönnemann 4093bb2dbe feat: initial 2015-07-08 20:41:36 +02:00
Stephan Bönnemann c59b5cac9c feat(nerf-dart): copy in nerf-dart module from npm 2015-06-19 18:35:25 -07:00
Stephan Bönnemann 44a18fd5cf chore(package): install commit analyzer and release note generator 2015-06-19 17:33:27 -07:00
Stephan Bönnemann e24c7c3833 chore(coverage): switch to coveralls 2015-06-18 16:48:28 -07:00
Stephan Bönnemann 057e8e9759 feat(plugins): normalize plugins and have unified function signature 2015-06-18 16:48:28 -07:00
Stephan Bönnemann da23c15ea5 test(plugins): initial suite 2015-06-18 16:10:55 -07:00
Stephan Bönnemann b63a43fa53 feat(verify): initial verification for pkg, options and env 2015-06-17 15:42:14 -07:00
Stephan Bönnemann c47ff97ab3 test(verify): initial suite 2015-06-17 15:41:26 -07:00
Stephan Bönnemann 689f37ae2e test(specs): how about running ALL tests? 2015-06-14 21:50:53 -07:00
Stephan Bönnemann a4fe18cade feat(pre): plugging last-release, commits and type together 2015-06-14 21:49:50 -07:00
Stephan Bönnemann 7412401d55 test(pre): initial suite 2015-06-14 21:49:50 -07:00
Stephan Bönnemann 6df67828fe refactor(commits,type): simplify interfaces 2015-06-14 21:49:39 -07:00
Stephan Bönnemann 66bf8ff710 refactor(mocks): extract from specs 2015-06-14 21:07:39 -07:00
Stephan Bönnemann a405021d08 feat(type): initial functionality 2015-06-14 19:40:05 -07:00
Stephan Bönnemann f79e26dcd6 test(type): initial suite 2015-06-14 19:39:38 -07:00
Stephan Bönnemann 861debb85e feat(commits): initial functionality 2015-06-14 18:17:25 -07:00
Stephan Bönnemann d6172b8767 test(commits): initial suite 2015-06-14 18:17:25 -07:00
Stephan Bönnemann f62525ad7e feat(last-release): initial functionality 2015-06-14 18:17:25 -07:00
Stephan Bönnemann cce860ec03 test(last-release): initial suite 2015-06-14 18:17:25 -07:00
Stephan Bönnemann 56238887d3 chore: config files 2015-06-14 18:17:25 -07:00
Stephan Bönnemann ac7037d948 chore: root 2015-06-12 18:55:23 -07:00
40 changed files with 1183 additions and 208 deletions
+11
View File
@@ -0,0 +1,11 @@
root = true
[*]
end_of_line = lf
insert_final_newline = true
indent_style = space
indent_size = 2
trim_trailing_whitespace = true
[*.md]
trim_trailing_whitespace = false
+12
View File
@@ -1,2 +1,14 @@
# common
coverage
node_modules
test/registry/couch
test/registry/data
*.log
*.dump
.DS_Store
.nyc_output
.test
.tmp
# build-artifacts
dist
+18
View File
@@ -0,0 +1,18 @@
# common
coverage
node_modules
test/registry/couch
test/registry/data
*.log
*.dump
.DS_Store
.nyc_output
.test
.tmp
# source/config
src
*.yml
.gitignore
.editorconfig
.npmrc
+16 -14
View File
@@ -1,20 +1,22 @@
language: node_js
node_js:
- '0.10'
sudo: false
language: node_js
cache:
directories:
- node_modules
notifications:
email: false
before_deploy:
- ./bin/semantic-release pre
deploy:
provider: npm
email: stephan@boennemann.me
skip_cleanup: true
api_key:
secure: Lq1FZweaNr/Ah8M7h3oWQ59HTswzvcksTUfzWb/dpqNn5FECN9I+4gi79EQPD62ZbJDCuDHY0EEEDLdnaB1z8N7kOj1RtmvC1k0teTEXFsGBPS4evXakRj+AfoBAGsTaXHqBYBOvYBxJT2Md/UoOgPSQjtAdZSNsmoFb1xkngQ0=
on:
branch: master
repo: boennemann/semantic-release
node_js:
- iojs-v2
- iojs-v1
- '0.12'
- '0.10'
before_install:
- npm i -g npm@^2.0.0
before_script:
- npm prune
- curl -Lo travis_after_all.py https://git.io/vLSON
after_success:
- python travis_after_all.py
- export $(cat .to_export_back)
- if [[ $BUILD_LEADER = YES ]]; then npm run coverage:upload; fi
- npm run semantic-release
+12 -18
View File
@@ -1,26 +1,20 @@
# semantic-release
[![Build Status](https://travis-ci.org/boennemann/semantic-release.svg)](https://travis-ci.org/boennemann/semantic-release)
[![Dependency Status](https://david-dm.org/boennemann/semantic-release.svg)](https://david-dm.org/boennemann/semantic-release)
[![devDependency Status](https://david-dm.org/boennemann/semantic-release/dev-status.svg)](https://david-dm.org/boennemann/semantic-release#info=devDependencies)
[![Build Status](https://travis-ci.org/semantic-release/semantic-release.svg?branch=next)](https://travis-ci.org/semantic-release/semantic-release)
[![Coverage Status](https://coveralls.io/repos/semantic-release/semantic-release/badge.svg?branch=next&service=github)](https://coveralls.io/github/semantic-release/semantic-release?branch=next)
[![Dependency Status](https://david-dm.org/semantic-release/semantic-release/next.svg)](https://david-dm.org/semantic-release/semantic-release/next)
[![devDependency Status](https://david-dm.org/semantic-release/semantic-release/next/dev-status.svg)](https://david-dm.org/semantic-release/semantic-release/next#info=devDependencies)
[![js-standard-style](https://img.shields.io/badge/code%20style-standard-brightgreen.svg?style=flat)](https://github.com/feross/standard)
[![NPM](https://nodei.co/npm/semantic-release.png?downloads=true&downloadRank=true&stars=true)](https://nodei.co/npm/semantic-release/)
```bash
npm i semantic-release
```
## What is `semantic-release` about?
`semantic-release` provides "prepublish" and "postpublish" hooks so you automatically release the correct version.
At its core `semantic-release` is a set of conventions that gives you entirely automated, semver-compliant package publishing. _Coincidentally_ these conventions make sense on their own, like having meaningful commit messages.
Run `semantic-release pre` right before `npm publish` and `semantic-release post` right after.
[![JSConfBP Talk](https://cloud.githubusercontent.com/assets/908178/8032541/e9bf6300-0dd6-11e5-92c9-8a39211368af.png)](https://www.youtube.com/watch?v=tc2UgG5L7WM&index=6&list=PLFZ5NyC0xHDaaTy6tY9p0C0jd_rRRl5Zm)
The post hook is automatable in `package.json` with a "postpublish" script:
```json
"scripts": {
"postpublish": "semantic-release post"
}
```
> This talk gives you a complete introduction to the underlying concepts
This is currently not possible for the "prepublish" hook. See [npm/npm#7118](https://github.com/npm/npm/issues/7118).
MIT License
2015 Stephan Bönnemann
The next branch is a completely rewritten version of `semantic-release`, that isn't fully tested and documented yet.
It has lots and lots of great new features, but also breaks a lot of things.
Unless you're adventurous you should look at the [stable master branch](https://github.com/semantic-release/semantic-release/tree/master).
-48
View File
@@ -1,48 +0,0 @@
#!/usr/bin/env node
'use strict'
var argv = require('minimist')(process.argv.slice(2), {
alias: {
d: 'debug',
dry: 'debug',
t: 'token'
},
default: {
token: process.env.GH_TOKEN || process.env.TOKEN || process.env.GITHUB_TOKEN
}
})
if (~argv._.indexOf('pre')) {
// The prepublish script is also executed when the package's dependencies are installed
// In this case we abort the command and do nothing.
var npmArgv = process.env.npm_config_argv
if (npmArgv) {
npmArgv = JSON.parse(npmArgv)
if (~npmArgv.cooked.indexOf('install')) process.exit(0)
}
console.log('Determining new version')
return require('../src/pre')(argv, function (err, result) {
if (err) {
console.log('Something went wrong.')
throw err
}
if (!result) {
console.log('Nothing changed. Not publishing.')
process.exit(1)
}
console.log('Publishing v' + result)
})
}
if (~argv._.indexOf('post')) {
return require('../src/post')(argv, function (err) {
if (err) {
console.log('Something went wrong.')
throw err
}
})
}
+13
View File
@@ -0,0 +1,13 @@
#!/usr/bin/env node
/* istanbul ignore next */
try {
require('../dist')
} catch (err) {
if (err.code === 'MODULE_NOT_FOUND') {
require('babel/register')
require('../src')
} else {
console.log(err)
}
}
-20
View File
@@ -1,20 +0,0 @@
'use strict'
module.exports = function (commits) {
var type = null
commits.every(function (commit) {
if (commit.breaks.length) {
type = 'major'
return false
}
if (commit.type === 'feat') type = 'minor'
if (!type && commit.type === 'fix') type = 'patch'
return true
})
return type
}
-14
View File
@@ -1,14 +0,0 @@
'use strict'
var git = require('conventional-changelog/lib/git')
module.exports = function (cb) {
git.latestTag(function (err, from) {
if (err) return cb(err)
git.getCommits({from: from}, function (err, commits) {
if (err) return cb(err)
cb(null, commits)
})
})
}
-12
View File
@@ -1,12 +0,0 @@
'use strict'
var commits = require('./commits')
var analyze = require('./analyze')
module.exports = function (cb) {
commits(function (err, commits) {
if (err) return cb(err)
cb(null, analyze(commits))
})
}
-18
View File
@@ -1,18 +0,0 @@
'use strict'
var exec = require('child_process').exec
var unlink = require('fs').unlinkSync
module.exports = function (pkg, cb) {
if (!pkg.name) return cb('Package must have a name')
exec('npm show ' + pkg.name + ' version', function(err, stdout, stderr) {
if (err) unlink('./npm-debug.log')
if (err && /is not in the npm registry/m.test(stderr)) return cb(null, null, true)
if (err) return cb(err)
cb(null, stdout.trim())
})
}
+72 -18
View File
@@ -1,39 +1,93 @@
{
"name": "semantic-release",
"description": "semantic semver compliant package publishing",
"author": "Stephan Bönnemann <stephan@boennemann.me>",
"bin": "./bin/semantic-release",
"description": "automated semver compliant package publishing",
"author": "Stephan Bönnemann <stephan@boennemann.me> (http://boennemann.me)",
"bin": {
"semantic-release": "./bin/semantic-release.js"
},
"bugs": {
"url": "https://github.com/boennemann/semantic-release/issues"
"url": "https://github.com/semantic-release/semantic-release/issues"
},
"bundleDependencies": [
"@semantic-release/commit-analyzer",
"@semantic-release/condition-travis",
"@semantic-release/error",
"@semantic-release/release-notes-generator"
],
"config": {
"nyc": {
"exclude": [
".test",
"node_modules"
]
}
},
"dependencies": {
"conventional-changelog": "0.0.11",
"github": "^0.2.3",
"minimist": "^1.1.0",
"@semantic-release/commit-analyzer": "^2.0.0",
"@semantic-release/condition-travis": "^4.0.0",
"@semantic-release/error": "^1.0.0",
"@semantic-release/release-notes-generator": "^2.0.0",
"git-head": "^1.2.1",
"github": "^0.2.4",
"lodash": "^3.9.3",
"nopt": "^3.0.3",
"npm-registry-client": "^6.4.0",
"npmconf": "^2.1.2",
"npmlog": "^1.2.1",
"parse-github-repo-url": "^1.0.0",
"semver": "^4.2.0"
"require-relative": "^0.8.7",
"run-auto": "^1.1.2",
"semver": "^5.0.1"
},
"devDependencies": {
"standard": "^2.3.1"
"babel": "^5.5.8",
"coveralls": "^2.11.2",
"mkdirp": "^0.5.1",
"nixt": "^0.4.1",
"nock": "^2.5.0",
"npm-registry-couchapp": "^2.6.11",
"nyc": "^3.0.0",
"proxyquire": "^1.5.0",
"rimraf": "^2.4.0",
"standard": "^4.2.1",
"tap": "^1.3.1"
},
"homepage": "https://github.com/boennemann/semantic-release",
"homepage": "https://github.com/semantic-release/semantic-release/tree/next#readme",
"keywords": [
"release",
"publish",
"author",
"automation",
"changelog",
"module",
"package",
"publish",
"release",
"semver",
"version",
"changelog"
"version"
],
"license": "MIT",
"main": "index.js",
"main": "dist/index.js",
"publishConfig": {
"tag": "next"
},
"release": {
"branch": "next"
},
"repository": {
"type": "git",
"url": "https://github.com/boennemann/semantic-release.git"
"url": "git+https://github.com/semantic-release/semantic-release.git"
},
"scripts": {
"postpublish": "./bin/semantic-release post",
"test": "standard"
"build": "rimraf dist && mkdirp dist && babel src --out-dir dist",
"coverage": "nyc report",
"coverage:upload": "npm run coverage -- --reporter=lcovonly && coveralls < coverage/lcov.info",
"prepublish": "npm run build",
"pretest:integration": "npm run build && npm run test:build",
"pretest:unit": "npm run build && npm run test:build",
"semantic-release": "./bin/semantic-release.js pre && npm dedupe && npm publish && ./bin/semantic-release.js post",
"test": "npm run test:style && npm run test:unit",
"test:build": "rimraf .test && mkdirp .test && babel test --out-dir .test",
"test:integration": "tap --no-cov .test/scenarios/*.js",
"test:style": "standard",
"test:unit": "nyc tap --no-cov .test/specs/*.js"
}
}
+134
View File
@@ -0,0 +1,134 @@
const { readFileSync, writeFileSync } = require('fs')
const path = require('path')
const _ = require('lodash')
const log = require('npmlog')
const nopt = require('nopt')
const npmconf = require('npmconf')
const PREFIX = 'semantic-release'
const env = process.env
const pkg = JSON.parse(readFileSync('./package.json'))
const knownOptions = {
branch: String,
debug: Boolean,
'github-token': String,
'github-url': String,
'analyze-commits': [path, String],
'generate-notes': [path, String],
'verify-conditions': [path, String],
'verify-release': [path, String]
}
const options = _.defaults(
_.mapKeys(nopt(knownOptions), (value, key) => _.camelCase(key)),
pkg.release,
{
branch: 'master',
debug: !env.CI,
githubToken: env.GH_TOKEN || env.GITHUB_TOKEN,
githubUrl: env.GH_URL
}
)
const plugins = require('./lib/plugins')(options)
npmconf.load({}, (err, conf) => {
if (err) {
log.error(PREFIX, 'Failed to load npm config.', err)
process.exit(1)
}
let npm = {
auth: {
token: env.NPM_TOKEN
},
loglevel: conf.get('loglevel'),
registry: conf.get('registry'),
tag: (pkg.publishConfig || {}).tag || conf.get('tag') || 'latest'
}
if (npm.registry[npm.registry.length - 1] !== '/') npm.registry += '/'
log.level = npm.loglevel
const config = {PREFIX, log, env, pkg, options, plugins, npm}
log.verbose(PREFIX, 'options:', _.assign({
githubToken: options.githubToken ? '***' : undefined
}), options)
log.verbose(PREFIX, 'Verifying config.')
const errors = require('./lib/verify')(config)
errors.forEach((err) => log.error(PREFIX, `${err.message} ${err.code}`))
if (errors.length) process.exit(1)
if (options.argv.remain[0] === 'pre') {
log.verbose(PREFIX, 'Running pre-script.')
log.verbose(PREFIX, 'Veriying conditions.')
plugins.verifyConditions(config, (err) => {
if (err) {
log[options.debug ? 'warn' : 'error'](PREFIX, err.message)
if (!options.debug) process.exit(1)
}
const nerfDart = require('./lib/nerf-dart')(npm.registry)
let wroteNpmRc = false
if (env.NPM_TOKEN) {
conf.set(`${nerfDart}:_authToken`, '${NPM_TOKEN}', 'project')
wroteNpmRc = true
} else if (env.NPM_OLD_TOKEN && env.NPM_EMAIL) {
// Using the old auth token format is not considered part of the public API
// This might go away anytime (i.e. once we have a better testing strategy)
conf.set('_auth', '${NPM_OLD_TOKEN}', 'project')
conf.set('email', '${NPM_EMAIL}', 'project')
wroteNpmRc = true
}
conf.save('project', (err) => {
if (err) return log.error(PREFIX, 'Failed to save npm config.', err)
if (wroteNpmRc) log.verbose(PREFIX, 'Wrote authToken to .npmrc.')
require('./pre')(config, (err, release) => {
if (err) {
log.error(PREFIX, 'Failed to determine new version.')
const args = [PREFIX, (err.code ? `${err.code} ` : '') + err.message]
if (err.stack) args.push(err.stack)
log.error(...args)
process.exit(1)
}
const message = `Determined version ${release.version} as "${npm.tag}".`
log.verbose(PREFIX, message)
if (options.debug) {
log.error(PREFIX, `${message} Not publishing in debug mode.`, release)
process.exit(1)
}
writeFileSync('./package.json', JSON.stringify(_.assign(pkg, {
version: release.version
}), null, 2))
log.verbose(PREFIX, `Wrote version ${release.version} to package.json.`)
})
})
})
} else if (options.argv.remain[0] === 'post') {
log.verbose(PREFIX, 'Running post-script.')
require('./post')(config, (err, published, release) => {
if (err) {
log.error(PREFIX, 'Failed to publish release notes.', err)
process.exit(1)
}
log.verbose(PREFIX, `${published ? 'Published' : 'Generated'} release notes.`, release)
})
} else {
log.error(PREFIX, `Command "${options.argv.remain[0]}" not recognized. User either "pre" or "post"`)
}
})
+25
View File
@@ -0,0 +1,25 @@
const { exec } = require('child_process')
module.exports = function ({lastRelease}, cb) {
const from = lastRelease.gitHead
const range = (from ? from + '..' : '') + 'HEAD'
exec(
`git log -E --format=%H==SPLIT==%B==END== ${range}`,
(err, stdout) => {
if (err) return cb(err)
cb(null, String(stdout).split('==END==\n')
.filter((raw) => !!raw.trim())
.map((raw) => {
const data = raw.split('==SPLIT==')
return {
hash: data[0],
message: data[1]
}
}))
}
)
}
+26
View File
@@ -0,0 +1,26 @@
const SemanticReleaseError = require('@semantic-release/error')
const npmlog = require('npmlog')
const RegClient = require('npm-registry-client')
module.exports = function ({pkg, npm}, cb) {
npmlog.level = npm.loglevel || 'error'
const client = new RegClient({log: npmlog})
client.get(`${npm.registry}${pkg.name}`, {
auth: npm.auth
}, (err, data) => {
if (err && err.statusCode === 404) return cb(null, {})
if (err) return cb(err)
const version = data['dist-tags'][npm.tag]
if (!version) return cb(new SemanticReleaseError(`There is no release with the dist-tag "${npm.tag}" yet. Tag a version first.`, 'ENODISTTAG'))
cb(null, {
version,
gitHead: data.versions[version].gitHead,
tag: npm.tag
})
})
}
+14
View File
@@ -0,0 +1,14 @@
// borrowed from https://github.com/npm/npm/blob/master/lib/config/nerf-dart.js
const url = require('url')
/* istanbul ignore next */
module.exports = function toNerfDart (uri) {
let parsed = url.parse(uri)
delete parsed.protocol
delete parsed.auth
delete parsed.query
delete parsed.search
delete parsed.hash
return url.resolve(url.format(parsed), '.')
}
+4
View File
@@ -0,0 +1,4 @@
/* istanbul ignore next */
module.exports = function (config, options, cb) {
cb(null)
}
+20
View File
@@ -0,0 +1,20 @@
const relative = require('require-relative')
let exports = module.exports = function (options) {
return {
analyzeCommits: exports.normalize(options.analyzeCommits, '@semantic-release/commit-analyzer'),
generateNotes: exports.normalize(options.generateNotes, '@semantic-release/release-notes-generator'),
verifyConditions: exports.normalize(options.verifyConditions, '@semantic-release/condition-travis'),
verifyRelease: exports.normalize(options.verifyRelease, './plugin-noop')
}
}
exports.normalize = function (pluginConfig, fallback) {
if (typeof pluginConfig === 'string') return relative(pluginConfig).bind(null, {})
if (pluginConfig && (typeof pluginConfig.path === 'string')) {
return relative(pluginConfig.path).bind(null, pluginConfig)
}
return require(fallback).bind(null, pluginConfig)
}
+20
View File
@@ -0,0 +1,20 @@
const SemanticReleaseError = require('@semantic-release/error')
module.exports = function (config, cb) {
const { plugins, lastRelease } = config
plugins.analyzeCommits(config, (err, type) => {
if (err) return cb(err)
if (!type) {
return cb(new SemanticReleaseError(
'There are no relevant changes, so no new version is released.',
'ENOCHANGE'
))
}
if (!lastRelease.version) return cb(null, 'initial')
cb(null, type)
})
}
+44
View File
@@ -0,0 +1,44 @@
const parseSlug = require('parse-github-repo-url')
const SemanticReleaseError = require('@semantic-release/error')
module.exports = function ({pkg, options, env}) {
let errors = []
if (!pkg.name) {
errors.push(new SemanticReleaseError(
'No "name" found in package.json.',
'ENOPKGNAME'
))
}
if (!pkg.repository || !pkg.repository.url) {
errors.push(new SemanticReleaseError(
'No "repository" found in package.json.',
'ENOPKGREPO'
))
} else if (!parseSlug(pkg.repository.url)) {
errors.push(new SemanticReleaseError(
'The "repository" field in the package.json is malformed.',
'EMALFORMEDPKGREPO'
))
}
if (options.debug) return errors
if (!options.githubToken) {
errors.push(new SemanticReleaseError(
'No github token specified.',
'ENOGHTOKEN'
))
}
if (!(env.NPM_TOKEN || (env.NPM_OLD_TOKEN && env.NPM_EMAIL))) {
errors.push(new SemanticReleaseError(
'No npm token specified.',
'ENONPMTOKEN'
))
}
return errors
}
+28 -28
View File
@@ -1,50 +1,50 @@
'use strict'
const url = require('url')
var exec = require('child_process').exec
var readFile = require('fs').readFileSync
const gitHead = require('git-head')
const GitHubApi = require('github')
const parseSlug = require('parse-github-repo-url')
var changelog = require('conventional-changelog')
var GitHubApi = require('github')
var parse = require('parse-github-repo-url')
module.exports = function (config, cb) {
const { pkg, options, plugins } = config
const ghConfig = options.githubUrl ? url.parse(options.githubUrl) : {}
var github = new GitHubApi({
version: '3.0.0'
})
const github = new GitHubApi({
version: '3.0.0',
port: ghConfig.port,
protocol: (ghConfig.protocol || '').split(':')[0] || null,
host: ghConfig.hostname
})
module.exports = function (options, cb) {
var pkg = JSON.parse(readFile('./package.json'))
var repository = pkg.repository ? pkg.repository.url : null
if (!repository) return cb('Package must have a repository')
changelog({
version: pkg.version,
repository: repository,
file: false
}, function(err, log) {
plugins.generateNotes(config, (err, log) => {
if (err) return cb(err)
exec('git rev-parse HEAD', function(err, hash) {
gitHead((err, hash) => {
if (err) return cb(err)
var ghRepo = parse(repository)
var release = {
const ghRepo = parseSlug(pkg.repository.url)
const release = {
owner: ghRepo[0],
repo: ghRepo[1],
tag_name: 'v' + pkg.version,
name: `v${pkg.version}`,
tag_name: `v${pkg.version}`,
target_commitish: hash,
draft: options.debug,
draft: !!options.debug,
body: log
}
if (options.debug && !options.githubToken) {
return cb(null, false, release)
}
github.authenticate({
type: 'oauth',
token: options.token
token: options.githubToken
})
github.releases.createRelease(release, function(err) {
github.releases.createRelease(release, (err) => {
if (err) return cb(err)
cb(null, true)
cb(null, true, release)
})
})
})
+37 -18
View File
@@ -1,26 +1,45 @@
'use strict'
const _ = require('lodash')
const auto = require('run-auto')
const semver = require('semver')
var fs = require('fs')
const getLastRelease = require('./lib/last-release')
const getCommits = require('./lib/commits')
const getType = require('./lib/type')
var semver = require('semver')
var type = require('../lib/type')
var version = require('../lib/version')
module.exports = function (options, cb) {
type(function (err, type) {
module.exports = function (config, cb) {
const {plugins} = config
auto({
lastRelease: getLastRelease.bind(null, config),
commits: ['lastRelease', (cb, results) => {
getCommits(_.assign({
lastRelease: results.lastRelease
}, config),
cb)
}],
type: ['commits', 'lastRelease', (cb, results) => {
getType(_.assign({
commits: results.commits,
lastRelease: results.lastRelease
}, config),
cb)
}]
}, (err, results) => {
if (err) return cb(err)
if (!type) return cb(null, null)
var path = './package.json'
var pkg = JSON.parse(fs.readFileSync(path))
version(pkg, function (err, version, unpublished) {
const nextRelease = {
type: results.type,
version: results.type === 'initial' ?
'1.0.0' :
semver.inc(results.lastRelease.version, results.type)
}
plugins.verifyRelease(_.assign({
commits: results.commits,
lastRelease: results.lastRelease,
nextRelease
}, config), (err) => {
if (err) return cb(err)
pkg.version = unpublished ? '1.0.0' : semver.inc(version, type)
if (!options.debug) fs.writeFileSync(path, JSON.stringify(pkg, null, 2))
cb(null, pkg.version)
cb(null, nextRelease)
})
})
}
+14
View File
@@ -0,0 +1,14 @@
const nixt = require('nixt')
module.exports = function (cwd, uri) {
return nixt()
.cwd(cwd)
.env('NPM_OLD_TOKEN', 'aW50ZWdyYXRpb246c3VjaHNlY3VyZQ==')
.env('NPM_EMAIL', 'integration@test.com')
.env('GH_TOKEN', 'ghtoken')
.env('CI', 'true')
.env('TRAVIS', 'true')
.env('TRAVIS_BRANCH', 'master')
.env('npm_config_registry', uri)
.clone()
}
+45
View File
@@ -0,0 +1,45 @@
const { exec } = require('child_process')
const { join } = require('path')
const { writeFileSync } = require('fs')
const mkdirp = require('mkdirp')
module.exports = function (name, cb) {
const cwd = join(__dirname, '../tmp', name)
mkdirp.sync(cwd)
writeFileSync(join(cwd, '.npmrc'), `
//localhost:1337/registry/_design/app/_rewrite/:username=integration
//localhost:1337/registry/_design/app/_rewrite/:email=integration@test.com`, null, 2)
writeFileSync(join(cwd, 'package.json'), JSON.stringify({
name,
repository: {
url: 'git+https://github.com/semantic-release/test'
},
_npmUser: {
name: 'integration',
email: 'integration@test.com'
},
maintainers: [{
name: 'integration',
email: 'integration@test.com'
}]
}, null, 2))
exec(`
git init &&
git config user.email "integration@test" &&
git config user.name "Integration Test" &&
git add . &&
git commit -m "chore: root"`
, {cwd}, (err, stdout, stderr) => {
if (err) {
console.log(stdout, stderr)
return cb(err)
}
cb(null, cwd)
})
}
+16
View File
@@ -0,0 +1,16 @@
const rawCommits = [
'hash-one==SPLIT==commit-one==END==\n',
'hash-two==SPLIT==commit-two==END==\n'
]
module.exports = {
exec: (command, cb) => {
cb(
null,
/\.\.HEAD/.test(command) ?
rawCommits[0] :
rawCommits.join()
)
},
'@noCallThru': true
}
+3
View File
@@ -0,0 +1,3 @@
module.exports = function (cb) {
cb(null, 'bar')
}
+6
View File
@@ -0,0 +1,6 @@
module.exports = () => ({
authenticate: () => true,
releases: {
createRelease: (release, cb) => cb(null)
}
})
+28
View File
@@ -0,0 +1,28 @@
const nock = require('nock')
const availableModule = {
'dist-tags': {
latest: '1.33.7',
foo: '0.8.15'
},
versions: {
'0.8.15': {
gitHead: 'bar'
},
'1.33.7': {
gitHead: 'HEAD'
}
}
}
module.exports = nock('http://registry.npmjs.org')
.get('/available')
.reply(200, availableModule)
.get('/tagged')
.reply(200, availableModule)
.get('/untagged')
.reply(200, availableModule)
.get('/@scoped/available')
.reply(200, availableModule)
.get('/unavailable')
.reply(404, {})
+12
View File
@@ -0,0 +1,12 @@
const { exec } = require('child_process')
const { join } = require('path')
const opts = {
cwd: join(__dirname, '../../test/registry')
}
module.exports = {
start: exec.bind(null, './start.sh', opts),
stop: exec.bind(null, './stop.sh', opts),
uri: 'http://localhost:15986/registry/_design/app/_rewrite/'
}
+19
View File
@@ -0,0 +1,19 @@
[couchdb]
database_dir = data
view_index_dir = data
delayed_commits = false
[couch_httpd_auth]
public_fields = appdotnet, avatar, avatarMedium, avatarLarge, date, email, fields, freenode, fullname, github, homepage, name, roles, twitter, type, _id, _rev
users_db_public = true
[httpd]
port = 15986
bind_address = 127.0.0.1
secure_rewrites = false
[log]
file = couch/couch.log
[admins]
admin = -pbkdf2-89582b49cd2e8443e29a841f5a76d367956a8e58,1afa2f1531a17ac97f2ac9e334293753,10
+36
View File
@@ -0,0 +1,36 @@
#!/bin/bash
# exit if an error occurs
set -e
cd $(dirname $0)
mkdir -p couch
# start couchdb as a background process, load local config, specify writable logfiles
if [[ $TRAVIS = true ]]
then
echo 'starting couch with sudo'
sudo couchdb -b -a local.ini -p couch/pid -o couch/stdout.log -e couch/stderr.log
else
couchdb -b -a local.ini -p couch/pid -o couch/stdout.log -e couch/stderr.log
fi
# wait for couch to start
sleep 5
COUCH=http://admin:password@127.0.0.1:15986
# create "registry" database
curl -X PUT $COUCH/registry
# create sample npm user
curl -X PUT $COUCH/_users/org.couchdb.user:integration -H Content-Type:application/json --data-binary '{"_id": "org.couchdb.user:integration","name": "integration","roles": [],"type": "user","password": "suchsecure","email": "integration@test.com"}'
# npm-registry-couchpp needs this variable set to run
export DEPLOY_VERSION=nope
# setup npm-registry-couchapp
npm explore npm-registry-couchapp -- npm start --npm-registry-couchapp:couch=$COUCH/registry
npm explore npm-registry-couchapp -- npm run load --npm-registry-couchapp:couch=$COUCH/registry
npm explore npm-registry-couchapp -- NO_PROMPT=yes npm run copy --npm-registry-couchapp:couch=$COUCH/registry
+12
View File
@@ -0,0 +1,12 @@
#!/bin/bash
# close couchdb background process
couchdb -d
# delete data and logs
cd $(dirname $0)
cat couch/{couch,stdout,stderr}.log
rm -rf couch
rm -rf data
+98
View File
@@ -0,0 +1,98 @@
const { join } = require('path')
const { test, tearDown } = require('tap')
const rimraf = require('rimraf')
const registry = require('../registry')
const testModule = require('../lib/test-module')
const baseScenario = require('../lib/base-scenario')
test('change version', (t) => {
t.plan(7)
registry.start((err) => {
t.error(err, 'registry started')
if (err) {
t.end()
t.bailout('registry not started')
}
testModule('change-version', (err, cwd) => {
t.error(err, 'test-module created')
if (err) {
t.end()
t.bailout('test-module not created')
}
t.test('no version', (tt) => {
tt.plan(1)
baseScenario(cwd, registry.uri)
.env('npm_config_loglevel', 'info')
.run('node ../../../bin/semantic-release.js pre')
.stderr(/ENOCHANGE/)
.code(1)
.end(tt.error)
})
t.test('initial version', (tt) => {
tt.plan(1)
baseScenario(cwd, registry.uri)
.exec('git commit -m "feat: initial" --allow-empty')
.exec('node ../../../bin/semantic-release.js pre')
.run('npm publish')
.stdout(/1\.0\.0/)
.code(0)
.end(tt.error)
})
t.test('patch version', (tt) => {
tt.plan(1)
baseScenario(cwd, registry.uri)
.exec('git commit -m "fix: foo" --allow-empty')
.exec('node ../../../bin/semantic-release.js pre')
.run('npm publish')
.stdout(/1\.0\.1/)
.code(0)
.end(tt.error)
})
t.test('feature version', (tt) => {
tt.plan(1)
baseScenario(cwd, registry.uri)
.exec('git commit -m "feat: foo" --allow-empty')
.exec('node ../../../bin/semantic-release.js pre')
.run('npm publish')
.code(0)
.stdout(/1\.1\.0/)
.end(tt.error)
})
t.test('breaking version', (tt) => {
tt.plan(1)
baseScenario(cwd, registry.uri)
.exec('git commit -m "feat: foo\n\n BREAKING CHANGE: bar" --allow-empty')
.exec('node ../../../bin/semantic-release.js pre')
.run('npm publish')
.code(0)
.stdout(/2\.0\.0/)
.end(tt.error)
})
})
})
})
tearDown(() => {
function cb (err, stdout, stderr) {
if (err) console.log(err)
if (stdout) console.log(stdout)
if (stderr) console.log(stderr)
}
rimraf(join(__dirname, '../tmp'), cb)
registry.stop(cb)
})
+32
View File
@@ -0,0 +1,32 @@
const test = require('tap').test
const proxyquire = require('proxyquire')
const commits = proxyquire('../../dist/lib/commits', {
'child_process': require('../mocks/child-process')
})
test('commits since last release', (t) => {
t.test('get all commits', (tt) => {
commits({lastRelease: {}}, (err, commits) => {
tt.error(err)
tt.is(commits.length, 2, 'all commits')
tt.is(commits[0].hash, 'hash-one', 'parsed hash')
tt.is(commits[1].message, 'commit-two', 'parsed message')
tt.end()
})
})
t.test('get commits since hash', (tt) => {
commits({lastRelease: {gitHead: 'hash'}}, (err, commits) => {
tt.error(err)
tt.is(commits.length, 1, 'specified commits')
tt.is(commits[0].hash, 'hash-one', 'parsed hash')
tt.is(commits[0].message, 'commit-one', 'parsed message')
tt.end()
})
})
t.end()
})
+79
View File
@@ -0,0 +1,79 @@
const { defaults } = require('lodash')
const test = require('tap').test
require('../mocks/registry')
const lastRelease = require('../../dist/lib/last-release')
const npm = {
registry: 'http://registry.npmjs.org/',
tag: 'latest'
}
test('last release from registry', (t) => {
t.plan(5)
t.test('get release from package name', (tt) => {
lastRelease({
pkg: {name: 'available'},
npm
}, (err, release) => {
tt.error(err)
tt.is(release.version, '1.33.7', 'version')
tt.is(release.gitHead, 'HEAD', 'gitHead')
tt.is(release.tag, 'latest', 'dist-tag')
tt.end()
})
})
t.test('get release from a tagged package\'s name', (tt) => {
lastRelease({
pkg: {name: 'tagged'},
npm: defaults({tag: 'foo'}, npm)
}, (err, release) => {
tt.error(err)
tt.is(release.version, '0.8.15', 'version')
tt.is(release.gitHead, 'bar', 'gitHead')
tt.is(release.tag, 'foo', 'dist-tag')
tt.end()
})
})
t.test('get error from an untagged package\'s name', (tt) => {
lastRelease({
pkg: {name: 'untagged'},
npm: defaults({tag: 'bar'}, npm)
}, (err) => {
tt.is(err.code, 'ENODISTTAG', 'error')
tt.end()
})
})
t.test('get release from scoped package name', (tt) => {
lastRelease({
pkg: {name: '@scoped/available'},
npm
}, (err, release) => {
tt.error(err)
tt.is(release.version, '1.33.7', 'version')
tt.is(release.gitHead, 'HEAD', 'gitHead')
tt.is(release.tag, 'latest', 'dist-tag')
tt.end()
})
})
t.test('get nothing from not yet published package name', (tt) => {
lastRelease({
pkg: {name: 'unavailable'},
npm
}, (err, release) => {
tt.error(err)
tt.is(release.version, undefined, 'no version')
tt.end()
})
})
})
+43
View File
@@ -0,0 +1,43 @@
const test = require('tap').test
const plugins = require('../../dist/lib/plugins')
test('export plugins', (t) => {
t.plan(4)
const defaultPlugins = plugins({})
t.is(typeof defaultPlugins.analyzeCommits, 'function')
t.is(typeof defaultPlugins.generateNotes, 'function')
t.is(typeof defaultPlugins.verifyConditions, 'function')
t.is(typeof defaultPlugins.verifyRelease, 'function')
})
test('normalize and load plugin', (t) => {
t.test('load from string', (tt) => {
const plugin = plugins.normalize('./dist/lib/plugin-noop')
tt.is(typeof plugin, 'function')
tt.end()
})
t.test('load from object', (tt) => {
const plugin = plugins.normalize({
path: './dist/lib/plugin-noop'
})
tt.is(typeof plugin, 'function')
tt.end()
})
t.test('load from object', (tt) => {
const plugin = plugins.normalize(null, '../../dist/lib/plugin-noop')
tt.is(typeof plugin, 'function')
tt.end()
})
})
+68
View File
@@ -0,0 +1,68 @@
const { defaults } = require('lodash')
const test = require('tap').test
const proxyquire = require('proxyquire')
const post = proxyquire('../../dist/post', {
'git-head': require('../mocks/git-head'),
github: require('../mocks/github')
})
const pkg = {
version: '1.0.0',
repository: {url: 'http://github.com/whats/up.git'}
}
const plugins = {generateNotes: (pkg, cb) => cb(null, 'the log')}
const defaultRelease = {
owner: 'whats',
repo: 'up',
name: 'v1.0.0',
tag_name: 'v1.0.0',
target_commitish: 'bar',
body: 'the log'
}
test('full post run', (t) => {
t.test('in debug mode w/o token', (tt) => {
post({
options: {debug: true},
pkg,
plugins
}, (err, published, release) => {
tt.error(err)
tt.is(published, false)
tt.match(release, defaults({draft: true}, defaultRelease))
tt.end()
})
})
t.test('in debug mode w/token', (tt) => {
post({
options: {debug: true, githubToken: 'yo'},
pkg,
plugins
}, (err, published, release) => {
tt.error(err)
tt.is(published, true)
tt.match(release, defaults({draft: true}, defaultRelease))
tt.end()
})
})
t.test('production', (tt) => {
post({
options: {githubToken: 'yo'},
pkg,
plugins
}, (err, published, release) => {
tt.error(err)
tt.is(published, true)
tt.match(release, defaultRelease)
tt.end()
})
})
})
+50
View File
@@ -0,0 +1,50 @@
const test = require('tap').test
const proxyquire = require('proxyquire')
require('../mocks/registry')
const pre = proxyquire('../../dist/pre', {
'child_process': require('../mocks/child-process')
})
const plugins = {
verifyRelease: (release, cb) => cb(null, release),
analyzeCommits: (commits, cb) => cb(null, 'major')
}
const npm = {
registry: 'http://registry.npmjs.org/',
tag: 'latest'
}
test('full pre run', (t) => {
t.test('increase version', (tt) => {
tt.plan(3)
pre({
npm,
pkg: {name: 'available'},
plugins
}, (err, release) => {
tt.error(err)
tt.is(release.type, 'major')
tt.is(release.version, '2.0.0')
})
})
t.test('increase version', (tt) => {
tt.plan(3)
pre({
npm,
pkg: {name: 'unavailable'},
plugins
}, (err, release) => {
tt.error(err)
tt.is(release.type, 'initial')
tt.is(release.version, '1.0.0')
})
})
t.end()
})
+48
View File
@@ -0,0 +1,48 @@
const test = require('tap').test
const type = require('../../dist/lib/type')
test('get type from commits', (t) => {
t.test('get type from plugin', (tt) => {
tt.plan(2)
type({
commits: [{
hash: '0',
message: 'a'
}],
lastRelease: {version: '1.0.0'},
plugins: {analyzeCommits: (config, cb) => cb(null, 'major')}
}, (err, type) => {
tt.error(err)
tt.is(type, 'major')
})
})
t.test('error when no changes', (tt) => {
tt.plan(1)
type({
commits: [],
lastRelease: {},
plugins: {analyzeCommits: (config, cb) => cb(null, null)}
}, (err) => {
tt.is(err.code, 'ENOCHANGE')
})
})
t.test('initial version', (tt) => {
tt.plan(2)
type({
commits: [],
lastRelease: {},
plugins: {analyzeCommits: (config, cb) => cb(null, 'major')}
}, (err, type) => {
tt.error(err)
tt.is(type, 'initial')
})
})
t.end()
})
+68
View File
@@ -0,0 +1,68 @@
const test = require('tap').test
const verify = require('../../dist/lib/verify')
test('verify pkg, options and env', (t) => {
t.test('dry run verification', (tt) => {
const noErrors = verify({
options: {debug: true},
pkg: {
name: 'package',
repository: {
url: 'http://github.com/whats/up.git'
}
}
})
tt.is(noErrors.length, 0)
const errors = verify({
options: {debug: true},
pkg: {}
})
tt.is(errors.length, 2)
tt.is(errors[0].code, 'ENOPKGNAME')
tt.is(errors[1].code, 'ENOPKGREPO')
const errors2 = verify({
options: {debug: true},
pkg: {
name: 'package',
repository: {
url: 'lol'
}
}
})
tt.is(errors2.length, 1)
tt.is(errors2[0].code, 'EMALFORMEDPKGREPO')
tt.end()
})
t.test('publish verification', (tt) => {
const noErrors = verify({
env: {NPM_TOKEN: 'yo'},
options: {githubToken: 'sup'},
pkg: {
name: 'package',
repository: {
url: 'http://github.com/whats/up.git'
}
}
})
tt.is(noErrors.length, 0)
const errors = verify({env: {}, options: {}, pkg: {}})
tt.is(errors.length, 4)
tt.is(errors[0].code, 'ENOPKGNAME')
tt.is(errors[1].code, 'ENOPKGREPO')
tt.is(errors[2].code, 'ENOGHTOKEN')
tt.is(errors[3].code, 'ENONPMTOKEN')
tt.end()
})
})