├── .npmignore ├── .gitignore ├── .travis.yml ├── screenshot.png ├── test └── compare.js ├── lib └── compare.js ├── package.json ├── bin.js ├── LICENSE ├── README.md └── index.js /.npmignore: -------------------------------------------------------------------------------- 1 | screenshot.png 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | yarn.lock 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 10 4 | - 12 5 | sudo: false 6 | -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/juliangruber/travis-watch/HEAD/screenshot.png -------------------------------------------------------------------------------- /test/compare.js: -------------------------------------------------------------------------------- 1 | const test = require('tap').test 2 | const compare = require('../lib/compare') 3 | 4 | test('compare', t => { 5 | t.deepEqual( 6 | [ 7 | { version: 'latest' }, 8 | { version: '4' }, 9 | { version: '0.12' }, 10 | { version: '6.0.2' }, 11 | { version: 'node' } 12 | ].sort(compare), 13 | [ 14 | { version: '0.12' }, 15 | { version: '4' }, 16 | { version: '6.0.2' }, 17 | { version: 'latest' }, 18 | { version: 'node' } 19 | ] 20 | ) 21 | t.end() 22 | }) 23 | -------------------------------------------------------------------------------- /lib/compare.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const semver = require('semver') 4 | 5 | const fixSemver = s => { 6 | const segs = String(s).split('.') 7 | segs[0] = segs[0] || '0' 8 | segs[1] = segs[1] || '0' 9 | segs[2] = segs[2] || '0' 10 | return segs.join('.') 11 | } 12 | 13 | module.exports = (a, b) => { 14 | const aSemver = fixSemver(a.version) 15 | const bSemver = fixSemver(b.version) 16 | let ret 17 | 18 | if (semver.valid(aSemver) && semver.valid(bSemver)) { 19 | ret = semver.compare(aSemver, bSemver) 20 | if (ret === 0) return a.key.localeCompare(b.key) 21 | return ret 22 | } 23 | 24 | ret = a.version.localeCompare(b.version) 25 | if (ret === 0) return a.key.localeCompare(b.key) 26 | return ret 27 | } 28 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "travis-watch", 3 | "version": "1.17.4", 4 | "license": "MIT", 5 | "repository": "juliangruber/travis-watch", 6 | "dependencies": { 7 | "ansi-diff-stream": "^1.2.0", 8 | "gh-canonical-repository": "^2.0.0", 9 | "git-current-commit": "^1.0.0", 10 | "render-ci-matrix": "^2.2.0", 11 | "semver": "^7.0.0", 12 | "sort-keys": "^4.0.0", 13 | "travis-ci": "^2.1.1" 14 | }, 15 | "devDependencies": { 16 | "prettier-standard": "^16.0.0", 17 | "standard": "^14.1.0", 18 | "tap": "^12.0.0" 19 | }, 20 | "scripts": { 21 | "test": "prettier-standard '*.js' 'test/*.js' && standard && tap test/*.js" 22 | }, 23 | "bin": { 24 | "travis-watch": "bin.js" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /bin.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 'use strict' 3 | 4 | const differ = require('ansi-diff-stream') 5 | const render = require('render-ci-matrix')() 6 | const resolve = require('path').resolve 7 | const fs = require('fs') 8 | const Watch = require('.') 9 | 10 | const diff = differ() 11 | diff.pipe(process.stdout) 12 | 13 | const dir = resolve(process.argv[2] || '.') 14 | 15 | try { 16 | fs.statSync(dir) 17 | } catch (err) { 18 | console.error('Usage: travis-watch [DIRECTORY]') 19 | process.exit(1) 20 | } 21 | 22 | try { 23 | fs.statSync(`${dir}/.travis.yml`) 24 | } catch (err) { 25 | console.error('Travis not set up. Skipping...') 26 | process.exit(0) 27 | } 28 | 29 | const watch = new Watch(dir) 30 | watch.start() 31 | watch.on('finish', () => { 32 | diff.reset() 33 | diff.write(render(watch.state)) 34 | process.exit(!watch.state.success) 35 | }) 36 | 37 | setInterval(() => { 38 | diff.reset() // FIXME 39 | diff.write(render(watch.state)) 40 | }, 100) 41 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Julian Gruber 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # travis-watch [![Build Status](https://travis-ci.org/juliangruber/travis-watch.svg?branch=master)](https://travis-ci.org/juliangruber/travis-watch) [![Greenkeeper badge](https://badges.greenkeeper.io/juliangruber/travis-watch.svg)](https://greenkeeper.io/) 3 | 4 | Stream live travis test results of the current commit to your terminal. Exits with the proper exit code too! 5 | 6 | ![screenshot](screenshot.png) 7 | 8 | ## Installation 9 | 10 | ```bash 11 | $ npm install -g travis-watch 12 | ``` 13 | 14 | ## Usage 15 | 16 | ```bash 17 | $ travis-watch --help 18 | Usage: travis-watch [DIRECTORY] 19 | ``` 20 | 21 | ## Supported build environments 22 | 23 | - Node.js 24 | - Ruby 25 | - PHP 26 | - Go 27 | - Python 28 | 29 | For more, please [open an issue](https://github.com/juliangruber/travis-watch/issues/new). 30 | 31 | ## JavaScript API 32 | 33 | ```js 34 | const differ = require('ansi-diff-stream') 35 | const render = require('render-ci-matrix')() 36 | const Watch = require('travis-watch') 37 | 38 | const diff = differ() 39 | diff.pipe(process.stdout) 40 | 41 | const watch = new Watch(process.cwd()) 42 | watch.start() 43 | 44 | setInterval( 45 | () => diff.write(render(watch.state)), 46 | 100 47 | ) 48 | 49 | watch.on('finish', () => { 50 | diff.write(render(watch.state)) 51 | process.exit(!watch.state.success) 52 | }) 53 | ``` 54 | 55 | ## Kudos 56 | 57 | - Development of this module is sponsored by the [Dat Project](https://datproject.org/). 58 | - Travis is :heart: 59 | 60 | ## Related 61 | 62 | - __[appveyor-watch](https://github.com/juliangruber/appveyor-watch)__ — Stream live AppVeyor test results of the current commit to your terminal! 63 | - __[ci-watch](https://github.com/juliangruber/ci-watch)__ — Travis-Watch and AppVeyor-Watch combined! 64 | - __[travis-logs](https://github.com/juliangruber/travis-logs)__ — Stream live travis logs to your terminal! 65 | - __[ansi-diff-stream](https://github.com/mafintosh/ansi-diff-stream)__ — A transform stream that diffs input buffers and outputs the diff as ANSI. If you pipe this to a terminal it will update the output with minimal changes 66 | - __[render-ci-matrix](https://github.com/juliangruber/render-ci-matrix)__ — Render a CI results matrix to the terminal. 67 | 68 | ## Sponsors 69 | 70 | This module is proudly supported by my [Sponsors](https://github.com/juliangruber/sponsors)! 71 | 72 | Do you want to support modules like this to improve their quality, stability and weigh in on new features? Then please consider donating to my [Patreon](https://www.patreon.com/juliangruber). Not sure how much of my modules you're using? Try [feross/thanks](https://github.com/feross/thanks)! 73 | 74 | ## License 75 | 76 | MIT 77 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const Travis = require('travis-ci') 4 | const getCommit = require('git-current-commit').sync 5 | const EventEmitter = require('events') 6 | const inherits = require('util').inherits 7 | const sort = require('sort-keys') 8 | const cmp = require('./lib/compare') 9 | const canonical = require('gh-canonical-repository') 10 | 11 | const travis = new Travis({ version: '2.0.0' }) 12 | 13 | module.exports = Watch 14 | inherits(Watch, EventEmitter) 15 | 16 | function Watch (dir) { 17 | if (!(this instanceof Watch)) return new Watch(dir) 18 | EventEmitter.call(this) 19 | 20 | this._dir = dir 21 | this.state = { 22 | started: new Date(), 23 | commit: { 24 | sha: getCommit(dir), 25 | found: false 26 | }, 27 | link: null, 28 | repo: null, 29 | build: null, 30 | results: {}, 31 | success: null 32 | } 33 | } 34 | 35 | Watch.prototype._getBuilds = function (cb) { 36 | const onrepo = (err, repo) => { 37 | if (err) return cb(err) 38 | this.state.repo = repo 39 | travis 40 | .repos(this.state.repo[0], this.state.repo[1]) 41 | .builds.get({ event_type: 'push' }, cb) 42 | } 43 | 44 | if (this.state.repo) onrepo(null, this.state.repo) 45 | else canonical(this._dir, onrepo, onrepo) 46 | } 47 | 48 | Watch.prototype._findCommit = function (commits) { 49 | return commits.find(c => c.sha === this.state.commit.sha) 50 | } 51 | 52 | Watch.prototype._findBuild = function (builds) { 53 | return builds.find(b => b.commit_id === this.state.commit.id) 54 | } 55 | 56 | Watch.prototype._link = function () { 57 | return [ 58 | 'https://travis-ci.org', 59 | this.state.repo[0], 60 | this.state.repo[1], 61 | 'builds', 62 | this.state.build.id 63 | ].join('/') 64 | } 65 | 66 | Watch.prototype._getBuild = function (cb) { 67 | this._getBuilds((err, res) => { 68 | if (err) return cb(err) 69 | if (!res.builds.length) return setTimeout(() => this._getBuild(cb), 500) 70 | const commit = this._findCommit(res.commits) 71 | if (!commit) return setTimeout(() => this._getBuild(cb), 1000) 72 | this.state.commit = { 73 | sha: commit.sha, 74 | id: commit.id, 75 | found: true, 76 | branch: commit.branch 77 | } 78 | const build = this._findBuild(res.builds) 79 | if (!build) return this._getBuild(cb) 80 | this.state.build = { 81 | id: build.id, 82 | number: build.number, 83 | startedAt: build.started_at, 84 | finishedAt: build.finished_at, 85 | job_ids: build.job_ids 86 | } 87 | this.state.link = this._link() 88 | cb() 89 | }) 90 | } 91 | 92 | const fixOSXBug = job => { 93 | if ( 94 | job.config.os === 'osx' && 95 | job.state === 'started' && 96 | new Date(job.started_at) > new Date() 97 | ) { 98 | job.state = 'created' 99 | } 100 | } 101 | 102 | const getJob = (id, cb) => { 103 | travis.jobs(id).get((err, res) => { 104 | if (err) return cb(err) 105 | fixOSXBug(res.job) 106 | cb(null, res.job) 107 | }) 108 | } 109 | 110 | const getJobKey = job => JSON.stringify(job.config) 111 | 112 | const getLanguageVersion = job => { 113 | if (job.config.language === 'ruby') { 114 | return String(job.config.rvm) 115 | } else if ( 116 | job.config[job.config.language] && 117 | job.config.language !== 'android' 118 | ) { 119 | return String(job.config[job.config.language]) 120 | } else { 121 | return '?' 122 | } 123 | } 124 | 125 | Watch.prototype.start = function () { 126 | this._getBuild(err => { 127 | if (err) return this.emit('error', err) 128 | 129 | let todo = this.state.build.job_ids.length 130 | 131 | this.state.build.job_ids.forEach(jobId => { 132 | const check = (err, job) => { 133 | if (err) return this.emit('error', err) 134 | job.version = getLanguageVersion(job) 135 | job.name = `${job.config.language}: ${job.version}` 136 | job.key = getJobKey(job) 137 | job.env = job.config.env 138 | job.startedAt = job.started_at 139 | job.allowFailure = job.allow_failure 140 | if (!this.state.results[job.config.os]) { 141 | this.state.results[job.config.os] = {} 142 | this.state.results = sort(this.state.results) 143 | } 144 | if (this.state.results[job.config.os][job.key]) { 145 | this.state.results[job.config.os][job.key] = job 146 | } else { 147 | this.state.results[job.config.os][job.key] = job 148 | Object.keys(this.state.results).forEach(os => { 149 | this.state.results[os] = sort(this.state.results[os], (a, b) => 150 | cmp(this.state.results[os][a], this.state.results[os][b]) 151 | ) 152 | }) 153 | } 154 | if (job.state === 'failed' && !job.allow_failure) { 155 | this.state.success = false 156 | } 157 | if ( 158 | job.state === 'started' || 159 | job.state === 'created' || 160 | job.state === 'received' || 161 | job.state === 'queued' 162 | ) { 163 | setTimeout(() => getJob(jobId, check), 1000) 164 | } else { 165 | if (!--todo) { 166 | if (typeof this.state.success !== 'boolean') { 167 | this.state.success = true 168 | } 169 | this.emit('finish') 170 | } 171 | } 172 | } 173 | getJob(jobId, check) 174 | }) 175 | }) 176 | } 177 | --------------------------------------------------------------------------------