Repository: bahmutov/now-pipeline
Branch: master
Commit: f0adf3e34e5a
Files: 16
Total size: 26.9 KB
Directory structure:
gitextract_mt_ry8fj/
├── .gitignore
├── .npmrc
├── .travis.yml
├── README.md
├── bin/
│ ├── list.js
│ ├── now-pipeline.js
│ └── prune.js
├── next-update-travis.sh
├── package.json
├── src/
│ ├── deploys-with-aliases.js
│ ├── index.js
│ ├── now-pipeline-spec.js
│ └── run-command.js
└── test/
├── index.js
├── package.json
└── subfolder/
└── foo.js
================================================
FILE CONTENTS
================================================
================================================
FILE: .gitignore
================================================
node_modules/
npm-debug.log
.DS_Store
================================================
FILE: .npmrc
================================================
registry=http://registry.npmjs.org/
save-exact=true
progress=false
================================================
FILE: .travis.yml
================================================
sudo: false
language: node_js
cache:
directories:
- node_modules
notifications:
email: true
node_js:
- '6'
before_script:
- npm prune
script:
- ./next-update-travis.sh
- npm test
# disable deploys for now - need new domain
# because current domain used for testing bahmutov.com
# is now a redirect to glebbahmutov.com :(
# - cd test
# - npm run deploy
# - npm run list
# - npm run prune
# - echo There should be no deploys now
# - npm run list
# # back to main folder
# - cd ..
after_success:
- npm run semantic-release
branches:
except:
- /^v\d+\.\d+\.\d+$/
================================================
FILE: README.md
================================================
# now-pipeline
> Single CI command to deploy new code to Zeit Now
> Includes e2e tests and the alias switch
[![NPM][npm-icon] ][npm-url]
[![Build status][ci-image] ][ci-url]
[![semantic-release][semantic-image] ][semantic-url]
[![js-standard-style][standard-image]][standard-url]
[](https://github.com/bahmutov/now-pipeline/labels/first-timers-only)
[![next-update-travis badge][nut-badge]][nut-readme]
## First time contributors
This repo is [first time OSS contributor friendly](http://www.firsttimersonly.com/).
See [these issues](https://github.com/bahmutov/now-pipeline/labels/first-timers-only)
to contribute in meaningful way.
## What and why
I am [super excited](https://glebbahmutov.com/blog/think-inside-the-box/)
about [Zeit Now](https://zeit.co/now) tool; this is the "missing CI tool"
for it. A single command `now-pipeline`
- deploys new version
- tests it
- switches alias to the new deployment
- takes down the old deployment
Should be enough to automatically update the server or service running in
the cloud without breaking anything.
## Install and use
```sh
npm i -g now-pipeline
```
Set `NOW_TOKEN` CI environment variable with a token that you can get from
[Zeit account page](https://zeit.co/account#api-tokens)
Add CI command to `now-pipeline`. By default it will execute `npm test`
and will pass the deployed url as `NOW_URL` environment variable. You can
customize everything.
## Example
Simple Travis commands
```yml
script:
# after unit tests
- npm i -g now-pipeline
- now-pipeline
```
Prune existing deploys (if they do not have an alias) and show the deploy.
```yml
script:
- npm i -g now-pipeline
- now-pipeline-prune
- now-pipeline
- now-pipeline-list
```
Set [domain alias](https://zeit.co/world) if there is no existing one
```yml
script:
- npm i -g now-pipeline
- now-pipeline --alias foo.domain.com
```
Pass in path to be used as deploy directory
```yml
script:
- npm i -g now-pipeline
- now-pipeline --dir your/directory
```
Pass test command and name of the environment variable for deployed url
```yml
script:
- npm i -g now-pipeline
- now-pipeline --as HOST --test "npm run e2e"
```
## Example projects
* [todomvc-express](https://github.com/bahmutov/todomvc-express/blob/master/.travis.yml)
* [express-sessions-tutorial](https://github.com/bahmutov/express-sessions-tutorial/blob/master/.travis.yml)
* [test-semantic-deploy-with-now](https://github.com/bahmutov/test-semantic-deploy-with-now)
## Additional bin commands
* `now-pipeline-list` - see the current deploys for the current project
* `now-pipeline-prune` - remove all non-aliased deploys for the current project
You can pass custom test command to the pipeline to be used after deploying
fresh install using `--test "command"` argument. The command will get `NOW_URL`
environment variable with new install. For example
```sh
npm i -g now-pipeline
now-pipeline --test "npm run prod-test"
```
where the `package.json` has
```json
{
"scripts": {
"prod-test": "e2e-test $NOW_URL"
}
}
```
## Debugging
You can see verbose log messages by running this tool with environment variable `DEBUG=now-pipeline`
## Details
* `now-pipeline` uses [Zeit API](https://zeit.co/api) via [now-client](https://github.com/zeit/now-client).
* You can see the list of recent actions at [Zeit dashboard](https://zeit.co/dashboard).
* It discovers files to send using [pkgd](https://github.com/inikulin/pkgd),
you can see the files by using the following command
(read [Smaller published NPM modules](https://glebbahmutov.com/blog/smaller-published-NPM-modules/) for more details)
```sh
t="$(npm pack .)"; wc -c "${t}"; tar tvf "${t}"; rm "${t}"
```
* file `.npmignore` is considered an optional file
## Related
* [next-update](https://github.com/bahmutov/next-update) is a similar
"if tests pass, upgrade" tool for your NPM dependencies.
### Small print
Author: Gleb Bahmutov <gleb.bahmutov@gmail.com> © 2016
* [@bahmutov](https://twitter.com/bahmutov)
* [glebbahmutov.com](http://glebbahmutov.com)
* [blog](http://glebbahmutov.com/blog)
License: MIT - do anything with the code, but don't blame me if it does not work.
Support: if you find any problems with this module, email / tweet /
[open issue](https://github.com/bahmutov/now-pipeline/issues) on Github
## MIT License
Copyright (c) 2016 Gleb Bahmutov <gleb.bahmutov@gmail.com>
Permission is hereby granted, free of charge, to any person
obtaining a copy of this software and associated documentation
files (the "Software"), to deal in the Software without
restriction, including without limitation the rights to use,
copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following
conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.
[npm-icon]: https://nodei.co/npm/now-pipeline.svg?downloads=true
[npm-url]: https://npmjs.org/package/now-pipeline
[ci-image]: https://travis-ci.org/bahmutov/now-pipeline.svg?branch=master
[ci-url]: https://travis-ci.org/bahmutov/now-pipeline
[semantic-image]: https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg
[semantic-url]: https://github.com/semantic-release/semantic-release
[standard-image]: https://img.shields.io/badge/code%20style-standard-brightgreen.svg
[standard-url]: http://standardjs.com/
[nut-badge]: https://img.shields.io/badge/next--update--travis-ok-green.svg
[nut-readme]: https://github.com/bahmutov/next-update-travis#readme
================================================
FILE: bin/list.js
================================================
#!/usr/bin/env node
'use strict'
require('console.table')
const nowPipeline = require('..')
const pkg = nowPipeline.getPackage()
nowPipeline.deployments(pkg.name)
.then(deploys => {
if (deploys.length) {
deploys.forEach(d => {
delete d.created
})
console.table(deploys)
} else {
console.log(`Zero deploys for ${pkg.name}`)
}
})
.catch(err => {
console.error(err)
process.exit(-1)
})
================================================
FILE: bin/now-pipeline.js
================================================
#!/usr/bin/env node
'use strict'
const debug = require('debug')('now-pipeline')
const initCrashReporter = require('node-sentry-error-reporter')
initCrashReporter()
const R = require('ramda')
const runCommand = require('../src/run-command')
const is = require('check-more-types')
const la = require('lazy-ass')
const pkgd = require('pkgd')
const argv = require('minimist')(process.argv.slice(2))
const path = require('path')
const envUrlName = 'NOW_URL'
const passAsName = argv.as || envUrlName
const testCommand = argv.test || 'npm test'
const nowPipeline = require('..')
const pkg = nowPipeline.getPackage()
function findFiles () {
if ('dir' in argv) {
// Pass directory with --dir flag
let dirArg = argv.dir
la(is.string(dirArg), 'directory path should be a string', dirArg)
try {
// change cwd to the passed directory path
process.chdir(path.resolve(argv.dir))
} catch (err) {
console.error('error changing deploy directory')
console.error(err)
console.error(`attempted directory: ${dirArg}`)
la(new Error(err))
}
}
debug('deploying from directory', process.cwd())
return pkgd(process.cwd())
.then(R.prop('files'))
}
var start
function findDeploy (url) {
la(is.url(url), 'expected url', url)
return nowPipeline.deployments()
.then(deploys => {
debug('looking for url %s in %d deploys', url, deploys.length)
const found = deploys.find(d => {
return url.endsWith(d.url)
})
if (!found) {
console.error('Could not find deploy for url', url)
process.exit(-1)
}
debug('found deployment', found)
return found
})
}
if (process.env[envUrlName]) {
const url = process.env[envUrlName]
console.log(`found existing env variable ${envUrlName} ${url}`)
// start = Promise.resolve(url)
// todo Find the deployment with given url
start = findFiles().then((filenames) => findDeploy(url))
} else {
start = findFiles().then(nowPipeline.deploy)
}
function setFullHost (deploy) {
if (!deploy.url) {
deploy.url = deploy.host
}
deploy.url = addHttps(deploy.url)
console.log('deployed to url', deploy.url)
la(is.url(deploy.url), 'expected deploy.url to be full https', deploy.url)
return deploy
}
function deployIsWorking (deploy) {
console.log(`deployed url ${deploy.url} is working`)
}
function addHttps (url) {
return url.startsWith('https://') ? url : 'https://' + url
}
const updateAliasIfNecessary = R.curry(
function updateAliasIfNecessary (aliasName, deploy) {
la(is.maybe.string(aliasName), 'alias name should be a string', aliasName)
return nowPipeline.deployments(pkg.name)
.then(deploys => {
return R.filter(R.prop('alias'))(deploys)
})
.then(deployed => {
console.log('found %d deploy(s) with aliases', deployed.length)
debug(deployed)
if (!deployed.length) {
console.log('there is no existing alias')
if (!aliasName) {
console.log('will skip updating alias to', deploy.url)
return
}
console.log('setting new alias to %s', aliasName)
return nowPipeline.now.createAlias(deploy.uid, aliasName)
.catch(err => {
console.error('Could not create alias')
console.error(err.message)
console.error('Note: you need to create a FIRST alias to domain MANUALLY')
console.error('using a command like this:')
console.error(` now alias ${deployed.url} ${aliasName}`)
return Promise.reject(err)
})
}
if (deployed.length > 1) {
console.log('found %d deployed aliases', deployed.length)
console.log('not sure which one to update')
return Promise.reject(new Error('Multiple aliases'))
}
la(deployed.length === 1, 'expect single alias')
const alias = deployed[0]
if (alias.uid === deploy.uid) {
console.log('The current alias %s points at the same deploy %s',
alias.alias, deploy.url)
console.log('Nothing to do')
return
}
console.log('switching alias %s to point at new deploy %s',
alias.alias, deploy.url)
la(is.unemptyString(alias.alias), 'invalid alias', alias)
return nowPipeline.now.createAlias(deploy.uid, alias.alias)
.then(result => {
debug('createAlias result', result)
console.log('switched alias %s to point at %s',
alias.alias, deploy.url)
console.log('taking down previously aliased deploy',
alias.uid)
return nowPipeline.remove(alias.uid)
})
.catch(error => {
console.error('error switching alias', deploy.uid, alias.alias)
console.error(error)
throw error
})
})
})
function testDeploy (deploy) {
la(is.object(deploy), 'wrong deploy object', deploy)
console.log('testing url %s', deploy.url)
console.log('passing it as env variable %s', passAsName)
console.log('test command "%s"', testCommand)
la(is.url(deploy.url), 'missing deploy url in', deploy)
const env = {}
env[passAsName] = deploy.url
return runCommand(testCommand, env)
.then(R.always(deploy))
}
start
.then(setFullHost)
.then(testDeploy)
.then(R.tap(deployIsWorking))
.then(updateAliasIfNecessary(argv.alias))
.catch(err => {
console.error('Something went wrong')
console.error('Sometimes restarting pipeline can help')
console.error(err)
process.exit(-1)
})
================================================
FILE: bin/prune.js
================================================
#!/usr/bin/env node
'use strict'
require('console.table')
const nowPipeline = require('..')
const pkg = nowPipeline.getPackage()
function nonAliasedDeploys (deploys, aliases) {
const aliasedDeploys = aliases.map(alias => alias.deploymentId)
return deploys.filter(deploy => {
return !aliasedDeploys.includes(deploy.uid)
})
}
Promise.all([
nowPipeline.deployments(pkg.name),
nowPipeline.aliases(pkg.name)
]).then(([deploys, aliases]) => {
if (deploys.length) {
console.table('Deploys', deploys)
} else {
console.log('No deploys')
}
if (aliases.length) {
console.table('Aliases', aliases)
} else {
console.log('No aliases')
}
const needToPrune = nonAliasedDeploys(deploys, aliases)
if (needToPrune.length) {
console.table('Will prune deploys', needToPrune)
} else {
console.log('No deploys to prune')
}
return needToPrune.reduce((prev, deploy) => {
return prev.then(() => {
console.log(`removing deploy ${deploy.uid} ${deploy.url}`)
return nowPipeline.remove(deploy.uid)
})
}, Promise.resolve())
}).then(() => {
console.log('Done pruning deploys')
}).catch(err => {
console.error(err)
process.exit(-1)
})
================================================
FILE: next-update-travis.sh
================================================
#!/bin/bash
set -e
if [ "$TRAVIS_EVENT_TYPE" = "cron" ]; then
if [ "$GH_TOKEN" = "" ]; then
echo ""
echo "⛔️ Cannot find environment variable GH_TOKEN ⛔️"
echo "Please set it up for this script to be able"
echo "to push results to GitHub"
echo "ℹ️ The best way is to use semantic-release to set it up"
echo ""
echo " https://github.com/semantic-release/semantic-release"
echo ""
echo "npm i -g semantic-release-cli"
echo "semantic-release-cli setup"
echo ""
exit 1
fi
echo "Upgrading dependencies using next-update"
npm i -g next-update
# you can edit options to allow only some updates
# --allow major | minor | patch
# --latest true | false
# see all options by installing next-update
# and running next-update -h
next-update --allow minor --latest false
git status
# if package.json is modified we have
# new upgrades
if git diff --name-only | grep package.json > /dev/null; then
echo "There are new versions of dependencies 💪"
git add package.json
echo "----------- package.json diff -------------"
git diff --staged
echo "-------------------------------------------"
git config --global user.email "next-update@ci.com"
git config --global user.name "next-update"
git commit -m "chore(deps): upgrade dependencies using next-update"
# push back to GitHub using token
git remote remove origin
# TODO read origin from package.json
# or use github api module github
# like in https://github.com/semantic-release/semantic-release/blob/caribou/src/post.js
git remote add origin https://next-update:$GH_TOKEN@github.com/bahmutov/now-pipeline.git
git push origin HEAD:master
else
echo "No new versions found ✋"
fi
else
echo "Not a cron job, normal test"
fi
================================================
FILE: package.json
================================================
{
"name": "now-pipeline",
"version": "0.0.0-semantic-release",
"description": "Single CI command to deploy new version to Zeit Now, including e2e tests and alias switch",
"author": "Gleb Bahmutov <gleb.bahmutov@gmail.com>",
"bugs": "https://github.com/bahmutov/now-pipeline/issues",
"config": {
"pre-git": {
"commit-msg": "simple",
"pre-commit": [
"npm prune",
"npm run deps",
"npm test",
"npm run ban"
],
"pre-push": [
"npm run secure",
"npm run license",
"npm run ban -- --all",
"npm run size"
],
"post-commit": [],
"post-merge": []
}
},
"engines": {
"node": ">=6"
},
"files": [
"bin",
"src/*.js",
"!src/*-spec.js"
],
"bin": {
"now-pipeline": "bin/now-pipeline.js",
"now-pipeline-list": "bin/list.js",
"now-pipeline-prune": "bin/prune.js"
},
"homepage": "https://github.com/bahmutov/now-pipeline#readme",
"keywords": [
"ci",
"now",
"test",
"tool",
"util",
"zeit"
],
"license": "MIT",
"main": "src/",
"noScopeName": "now-pipeline",
"publishConfig": {
"registry": "http://registry.npmjs.org/"
},
"repository": {
"type": "git",
"url": "https://github.com/bahmutov/now-pipeline.git"
},
"scripts": {
"ban": "ban",
"deps": "deps-ok",
"issues": "git-issues",
"license": "license-checker --production --onlyunknown --csv",
"lint": "standard --verbose --fix src/*.js bin/*.js",
"pretest": "npm run lint",
"secure": "nsp check",
"size": "t=\"$(npm pack .)\"; wc -c \"${t}\"; tar tvf \"${t}\"; rm \"${t}\";",
"test": "npm run unit",
"unit": "mocha src/*-spec.js",
"semantic-release": "semantic-release pre && npm publish && semantic-release post"
},
"devDependencies": {
"ban-sensitive-files": "1.9.2",
"deps-ok": "1.4.1",
"git-issues": "1.3.1",
"github-post-release": "1.13.1",
"license-checker": "15.0.0",
"mocha": "4.1.0",
"next-update-travis": "1.7.1",
"nsp": "3.2.1",
"pre-git": "3.17.1",
"semantic-release": "6.3.6",
"simple-commit-message": "3.3.2",
"standard": "10.0.3"
},
"dependencies": {
"check-more-types": "2.24.0",
"console.table": "0.10.0",
"cross-spawn": "5.1.0",
"debug": "3.2.6",
"lazy-ass": "1.6.0",
"minimist": "1.2.0",
"moment": "2.24.0",
"node-sentry-error-reporter": "1.8.0",
"now-client": "0.7.0",
"pkgd": "1.1.2",
"ramda": "0.26.1"
},
"release": {
"analyzeCommits": "simple-commit-message",
"generateNotes": "github-post-release"
}
}
================================================
FILE: src/deploys-with-aliases.js
================================================
'use strict'
const is = require('check-more-types')
const la = require('lazy-ass')
const debug = require('debug')('now-pipeline')
function combineDeploysAndAliases ({deploys, aliases}) {
la(is.array(deploys), 'list of deploys missing', deploys)
la(is.array(aliases), 'list of aliases missing', aliases)
debug('matching %d deploys with %d aliases', deploys.length, aliases.length)
return deploys.map(deploy => {
const alias = aliases.find(a => a.deploymentId === deploy.uid)
if (alias) {
deploy.alias = alias.alias
deploy.aliasId = alias.uid
debug('deploy %s matched alias %s', deploy.url, deploy.alias)
}
return deploy
})
}
module.exports = combineDeploysAndAliases
================================================
FILE: src/index.js
================================================
'use strict'
require('console.table')
const debug = require('debug')('now-pipeline')
const R = require('ramda')
const path = require('path')
const fs = require('fs')
const is = require('check-more-types')
const la = require('lazy-ass')
const Now = require('now-client')
const combineDeploysAndAliases = require('./deploys-with-aliases')
const moment = require('moment')
function getPackage () {
const packageFilename = path.join(process.cwd(), 'package.json')
const pkg = require(packageFilename)
return pkg
}
function addRelativeTimes (deploys) {
return deploys.map(deploy => {
// relative time without "ago" suffix
deploy.when = moment(Number(deploy.created)).fromNow(true)
return deploy
})
}
function sortByAge (deploys) {
return R.sortBy(
R.compose(Number, R.prop('created'))
)(deploys)
}
function nowApi () {
const authToken = process.env.NOW_TOKEN
if (!authToken) {
console.log('WARNING: Cannot find NOW_TOKEN environment variable')
}
const now = Now(authToken)
function wait (seconds) {
return new Promise((resolve, reject) => {
setTimeout(resolve, seconds * 1000)
})
}
function checkDeploy (id) {
la(is.unemptyString(id), 'expected deploy id', id)
return now.getDeployment(id)
}
function getDeploysAndAliases () {
return Promise.all([
now.getDeployments(),
now.getAliases()
]).then(([deploys, aliases]) => combineDeploysAndAliases({deploys, aliases}))
}
function isDeploying (state) {
return state === 'DEPLOYING' || state === 'BOOTED' || state === 'BUILDING'
}
function waitUntilDeploymentReady (id, secondsRemaining) {
la(is.number(secondsRemaining), 'wrong waiting limit', secondsRemaining)
const sleepSeconds = 5
return checkDeploy(id)
.then(r => {
console.log(r.state, r.host, 'limit', secondsRemaining, 'seconds')
if (r.state === 'READY') {
return r
}
if (isDeploying(r.state)) {
if (secondsRemaining < sleepSeconds) {
throw new Error('Deploy timed out\n' + JSON.stringify(r))
}
return wait(sleepSeconds)
.then(() => waitUntilDeploymentReady(id, secondsRemaining - sleepSeconds))
}
throw new Error('Something went wrong with the deploy\n' + JSON.stringify(r))
})
}
const api = {
now, // expose the actual now client
getPackage,
// lists current deploy optionally limited with given predicate
deployments (filter) {
if (is.string(filter)) {
filter = R.propEq('name', filter)
}
filter = filter || R.T
return getDeploysAndAliases()
.then(sortByAge)
.then(addRelativeTimes)
.then(R.filter(filter))
},
aliases () {
return now.getAliases()
},
remove (id) {
la(is.unemptyString(id), 'expected deployment id', id)
debug('deleting deployment %s', id)
return now.deleteDeployment(id)
},
/**
deploys given filenames. Returns object with result
{
uid: 'unique id',
host: 'now-pipeline-test-lqsibottrb.now.sh',
state: 'READY'
}
*/
deploy (filenames) {
debug('deploying %d files', filenames.length)
debug(filenames)
la(is.strings(filenames), 'missing file names', filenames)
la(is.not.empty(filenames), 'expected list of files', filenames)
// Files not required, but might be checked for by la
const optionalFiles = ['.npmignore']
filenames.forEach(name => {
if (!(optionalFiles.includes(name))) {
la(fs.existsSync(name), 'cannot find file', name)
}
})
const isPackageJson = R.test(/package\.json$/)
const packageJsonPresent = R.any(isPackageJson)
la(packageJsonPresent(filenames),
'missing package.json file in the list', filenames)
const packageJsonFilename = filenames.find(isPackageJson)
const packageJsonFolder = path.dirname(packageJsonFilename)
debug('package.json filename is', packageJsonFilename)
debug('in folder', packageJsonFolder)
// TODO make sure all files exist
const sources = R.map(name => fs.readFileSync(name, 'utf8'))(filenames)
const names = R.map(filename => {
return path.relative(packageJsonFolder, filename)
})(filenames)
debug('sending files', names)
const params = R.zipObj(names, sources)
// parsed JSON object
params.package = JSON.parse(params['package.json'])
delete params['package.json']
// we do not need dev dependencies in the deployed server
delete params.package.devDependencies
return now.createDeployment(params)
.then(r => {
// TODO make an option
const maxWaitSeconds = 60 * 10
return waitUntilDeploymentReady(r.uid, maxWaitSeconds)
})
.catch(r => {
if (is.error(r)) {
console.error('error during deployment')
console.error(r.message)
return Promise.reject(r)
}
console.error('error')
console.error(r.response.data)
return Promise.reject(new Error(r.response.data.err.message))
})
}
}
return api
}
const now = nowApi()
module.exports = now
//
// examples
//
// function showDeploysForProject () { // eslint-disable-line no-unused-vars
// const name = 'now-pipeline-test'
// now.deployments(R.propEq('name', name))
// .then(console.table).catch(console.error)
// }
// function showAllDeploys () { // eslint-disable-line no-unused-vars
// now.deployments().then(console.table).catch(console.error)
// }
// function deployTest () { // eslint-disable-line no-unused-vars
// const relative = require('path').join.bind(null, __dirname)
// const files = [
// relative('../test/package.json'),
// relative('../test/index.js')
// ]
// return now.deploy(files)
// }
// showAllDeploys()
// showDeploysForProject()
// deployTest()
// .then(result => {
// console.log('deployment done with result', result)
// })
// .catch(console.error)
================================================
FILE: src/now-pipeline-spec.js
================================================
'use strict'
/* global describe, it */
describe('now-pipeline', () => {
it('write this test!', () => {
})
})
================================================
FILE: src/run-command.js
================================================
'use strict'
const la = require('lazy-ass')
const is = require('check-more-types')
const spawn = require('cross-spawn')
function runCommand (command, extraEnv) {
if (is.string(command)) {
command = command.split(' ')
}
la(is.array(command), 'expected command and args array', command)
la(command.length > 0, 'missing command, needs at least something', command)
la(is.object(extraEnv), 'expected env object', extraEnv)
return new Promise(function (resolve, reject) {
const customEnv = Object.assign({}, process.env, extraEnv)
const spawnOptions = {
env: customEnv,
stdio: 'inherit'
}
const prog = command[0]
const args = command.slice(1)
console.log(`running "${prog}" with extra env keys`,
Object.keys(extraEnv))
const proc = spawn(prog, args, spawnOptions)
proc.on('error', (err) => {
console.error('prog error')
console.error(err)
reject(err)
})
proc.on('close', (code) => {
// debug(`${prog} exit code ${code}`)
if (code) {
const msg = `${prog} exit code ${code}`
console.error(msg)
return reject(new Error(msg))
}
resolve()
})
})
}
module.exports = runCommand
================================================
FILE: test/index.js
================================================
const port = process.env.PORT || 4000
const foo = require('./subfolder/foo')
require('http').Server((req, res) => {
const msg = "Hi there! " + foo + " node " + process.versions.node;
res.setHeader("Content-Type", "text/plain; charset=utf-8");
res.end(require("sign-bunny")(msg));
}).listen(port);
console.log('listening on port', port)
================================================
FILE: test/package.json
================================================
{
"name": "now-pipeline-test",
"version": "0.0.0",
"private": true,
"dependencies": {
"sign-bunny": "1.0.0"
},
"scripts": {
"start": "node index.js",
"test": "curl $NOW_URL",
"list": "node ../bin/list",
"prune": "node ../bin/prune",
"deploy": "DEBUG=now-pipeline node ../bin/now-pipeline --test 'npm run prod-test' --alias test.bahmutov.com",
"prod-test": "echo Testing deployed url; curl $NOW_URL"
}
}
================================================
FILE: test/subfolder/foo.js
================================================
module.exports = 'foo'
gitextract_mt_ry8fj/
├── .gitignore
├── .npmrc
├── .travis.yml
├── README.md
├── bin/
│ ├── list.js
│ ├── now-pipeline.js
│ └── prune.js
├── next-update-travis.sh
├── package.json
├── src/
│ ├── deploys-with-aliases.js
│ ├── index.js
│ ├── now-pipeline-spec.js
│ └── run-command.js
└── test/
├── index.js
├── package.json
└── subfolder/
└── foo.js
SYMBOL INDEX (13 symbols across 5 files)
FILE: bin/now-pipeline.js
function findFiles (line 24) | function findFiles () {
function findDeploy (line 49) | function findDeploy (url) {
function setFullHost (line 79) | function setFullHost (deploy) {
function deployIsWorking (line 90) | function deployIsWorking (deploy) {
function addHttps (line 94) | function addHttps (url) {
function testDeploy (line 165) | function testDeploy (deploy) {
FILE: bin/prune.js
function nonAliasedDeploys (line 10) | function nonAliasedDeploys (deploys, aliases) {
FILE: src/deploys-with-aliases.js
function combineDeploysAndAliases (line 7) | function combineDeploysAndAliases ({deploys, aliases}) {
FILE: src/index.js
function getPackage (line 15) | function getPackage () {
function addRelativeTimes (line 21) | function addRelativeTimes (deploys) {
function sortByAge (line 29) | function sortByAge (deploys) {
function nowApi (line 35) | function nowApi () {
FILE: src/run-command.js
function runCommand (line 7) | function runCommand (command, extraEnv) {
Condensed preview — 16 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (30K chars).
[
{
"path": ".gitignore",
"chars": 38,
"preview": "node_modules/\nnpm-debug.log\n.DS_Store\n"
},
{
"path": ".npmrc",
"chars": 68,
"preview": "registry=http://registry.npmjs.org/\nsave-exact=true\nprogress=false\n\n"
},
{
"path": ".travis.yml",
"chars": 605,
"preview": "sudo: false\nlanguage: node_js\ncache:\n directories:\n - node_modules\nnotifications:\n email: true\nnode_js:\n - '6'\nbef"
},
{
"path": "README.md",
"chars": 6210,
"preview": "# now-pipeline\n\n> Single CI command to deploy new code to Zeit Now\n> Includes e2e tests and the alias switch\n\n[![NPM][np"
},
{
"path": "bin/list.js",
"chars": 447,
"preview": "#!/usr/bin/env node\n\n'use strict'\n\nrequire('console.table')\n\nconst nowPipeline = require('..')\nconst pkg = nowPipeline.g"
},
{
"path": "bin/now-pipeline.js",
"chars": 5523,
"preview": "#!/usr/bin/env node\n\n'use strict'\n\nconst debug = require('debug')('now-pipeline')\nconst initCrashReporter = require('nod"
},
{
"path": "bin/prune.js",
"chars": 1198,
"preview": "#!/usr/bin/env node\n\n'use strict'\n\nrequire('console.table')\n\nconst nowPipeline = require('..')\nconst pkg = nowPipeline.g"
},
{
"path": "next-update-travis.sh",
"chars": 1805,
"preview": "#!/bin/bash\n\nset -e\n\nif [ \"$TRAVIS_EVENT_TYPE\" = \"cron\" ]; then\n if [ \"$GH_TOKEN\" = \"\" ]; then\n echo \"\"\n echo \"⛔️"
},
{
"path": "package.json",
"chars": 2634,
"preview": "{\n \"name\": \"now-pipeline\",\n \"version\": \"0.0.0-semantic-release\",\n \"description\": \"Single CI command to deploy new ver"
},
{
"path": "src/deploys-with-aliases.js",
"chars": 715,
"preview": "'use strict'\n\nconst is = require('check-more-types')\nconst la = require('lazy-ass')\nconst debug = require('debug')('now-"
},
{
"path": "src/index.js",
"chars": 6111,
"preview": "'use strict'\n\nrequire('console.table')\n\nconst debug = require('debug')('now-pipeline')\nconst R = require('ramda')\nconst "
},
{
"path": "src/now-pipeline-spec.js",
"chars": 114,
"preview": "'use strict'\n\n/* global describe, it */\ndescribe('now-pipeline', () => {\n it('write this test!', () => {\n })\n})\n"
},
{
"path": "src/run-command.js",
"chars": 1219,
"preview": "'use strict'\n\nconst la = require('lazy-ass')\nconst is = require('check-more-types')\nconst spawn = require('cross-spawn')"
},
{
"path": "test/index.js",
"chars": 342,
"preview": "const port = process.env.PORT || 4000\nconst foo = require('./subfolder/foo')\nrequire('http').Server((req, res) => {\n co"
},
{
"path": "test/package.json",
"chars": 445,
"preview": "{\n \"name\": \"now-pipeline-test\",\n \"version\": \"0.0.0\",\n \"private\": true,\n \"dependencies\": {\n \"sign-bunny\": \"1.0.0\"\n"
},
{
"path": "test/subfolder/foo.js",
"chars": 23,
"preview": "module.exports = 'foo'\n"
}
]
About this extraction
This page contains the full source code of the bahmutov/now-pipeline GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 16 files (26.9 KB), approximately 7.5k tokens, and a symbol index with 13 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.