diff --git a/README.md b/README.md index 8170c93..99d18ef 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ Realtime global deployments served over HTTP/2. You can find the FAQ [here](http ## Usage -Firstly, make sure to install the package: +Firstly, make sure to install the package globally: ```bash $ npm install -g now diff --git a/bin/now-deploy.js b/bin/now-deploy.js index 84f0922..0df1a5f 100755 --- a/bin/now-deploy.js +++ b/bin/now-deploy.js @@ -5,7 +5,7 @@ import {resolve} from 'path' // Packages import Progress from 'progress' -import {stat} from 'fs-promise' +import fs from 'fs-promise' import bytes from 'bytes' import chalk from 'chalk' import minimist from 'minimist' @@ -21,6 +21,7 @@ import Now from '../lib' import toHumanPath from '../lib/utils/to-human-path' import promptOptions from '../lib/utils/prompt-options' import {handleError, error} from '../lib/error' +import {onGitHub, isRepoPath, gitPathParts} from '../lib/github' import readMetaData from '../lib/read-metadata' const argv = minimist(process.argv.slice(2), { @@ -101,19 +102,15 @@ const help = () => { ${chalk.cyan('$ now /usr/src/project')} - ${chalk.gray('–')} Lists all deployments with their IDs + ${chalk.gray('–')} Deploys a GitHub repository - ${chalk.cyan('$ now ls')} + ${chalk.cyan('$ now user/repo#ref')} - ${chalk.gray('–')} Associates deployment ${chalk.dim('`deploymentId`')} with ${chalk.dim('`custom-domain.com`')} + ${chalk.gray('–')} Deploys a GitHub or GitLab repo using its URL - ${chalk.cyan('$ now alias deploymentId custom-domain.com')} + ${chalk.cyan('$ now https://gitlab.com/user/repo')} - ${chalk.gray('–')} Stores a secret - - ${chalk.cyan('$ now secret add mysql-password 123456')} - - ${chalk.gray('–')} Deploys with ENV vars (using the ${chalk.dim('`mysql-password`')} secret stored above) + ${chalk.gray('–')} Deploys with ENV vars ${chalk.cyan('$ now -e NODE_ENV=production -e MYSQL_PASSWORD=@mysql-password')} @@ -133,6 +130,9 @@ if (path) { path = process.cwd() } +// If the current deployment is a repo +const gitHubRepo = {} + const exit = code => { // we give stdout some time to flush out // because there's a node bug where @@ -192,16 +192,59 @@ if (argv.h || argv.help) { async function sync(token) { const start = Date.now() + const rawPath = argv._[0] - if (!quiet) { - console.log(`> Deploying ${chalk.bold(toHumanPath(path))}`) + const stopDeployment = msg => { + error(msg) + process.exit(1) } + const isValidRepo = isRepoPath(rawPath) + try { - await stat(path) + await fs.stat(path) } catch (err) { - error(`Could not read directory ${chalk.bold(path)}`) - process.exit(1) + let repo + + if (isValidRepo && isValidRepo !== 'no-valid-url') { + const searchMessage = setTimeout(() => { + console.log('> Didn\'t find directory. Searching on GitHub...') + }, 500) + + try { + repo = await onGitHub(rawPath, debug) + } catch (err) {} + + clearTimeout(searchMessage) + + const gitParts = gitPathParts(rawPath) + Object.assign(gitHubRepo, gitParts) + } + + if (repo) { + // Tell now which directory to deploy + path = repo.path + + // Set global variable for deleting tmp dir later + // once the deployment has finished + Object.assign(gitHubRepo, repo) + } else if (isValidRepo === 'no-valid-url') { + stopDeployment(`This URL is not a valid repository from GitHub or GitLab.`) + } else if (isValidRepo) { + const gitRef = gitHubRepo.ref ? `with "${chalk.bold(gitHubRepo.ref)}" ` : '' + stopDeployment(`There's no repository named "${chalk.bold(gitHubRepo.main)}" ${gitRef}on GitHub or GitLab`) + } else { + stopDeployment(`Could not read directory ${chalk.bold(path)}`) + } + } + + if (!quiet) { + if (gitHubRepo) { + const gitRef = gitHubRepo.ref ? ` at "${chalk.bold(gitHubRepo.ref)}" ` : '' + console.log(`> Deploying GitHub repository "${chalk.bold(gitHubRepo.main)}"` + gitRef) + } else { + console.log(`> Deploying ${chalk.bold(toHumanPath(path))}`) + } } let deploymentType @@ -227,7 +270,7 @@ async function sync(token) { isStatic = true } else { try { - await stat(resolve(path, 'package.json')) + await fs.stat(resolve(path, 'package.json')) } catch (err) { hasPackage = true } @@ -235,7 +278,7 @@ async function sync(token) { [hasPackage, hasDockerfile] = await Promise.all([ await (async () => { try { - await stat(resolve(path, 'package.json')) + await fs.stat(resolve(path, 'package.json')) } catch (err) { return false } @@ -243,7 +286,7 @@ async function sync(token) { })(), await (async () => { try { - await stat(resolve(path, 'Dockerfile')) + await fs.stat(resolve(path, 'Dockerfile')) } catch (err) { return false } @@ -485,6 +528,16 @@ function printLogs(host) { if (!quiet) { console.log(`${chalk.cyan('> Deployment complete!')}`) } + + if (gitHubRepo && gitHubRepo.cleanup) { + // Delete temporary directory that contains repository + gitHubRepo.cleanup() + + if (debug) { + console.log(`> [debug] Removed temporary repo directory`) + } + } + process.exit(0) }) } diff --git a/lib/github.js b/lib/github.js new file mode 100644 index 0000000..3df8000 --- /dev/null +++ b/lib/github.js @@ -0,0 +1,118 @@ +// Native +import path from 'path' +import url from 'url' + +// Packages +import fs from 'fs-promise' +import download from 'download' +import tmp from 'tmp-promise' +import isURL from 'is-url' + +const downloadRepo = async repoPath => { + const pathParts = gitPathParts(repoPath) + const url = `https://api.github.com/repos/${pathParts.main}/tarball/${pathParts.ref}` + + const tmpDir = await tmp.dir({ + // We'll remove it manually once deployment is done + keep: true, + // Recursively remove directory when calling respective method + unsafeCleanup: true + }) + + try { + await download(url, tmpDir.path, { + extract: true + }) + } catch (err) { + tmpDir.cleanup() + return false + } + + const tmpContents = await fs.readdir(tmpDir.path) + tmpDir.path = path.join(tmpDir.path, tmpContents[0]) + + return tmpDir +} + +const splittedURL = fullURL => { + const pathParts = url.parse(fullURL).path.split('/') + pathParts.shift() + + // Set path to repo... + const main = pathParts[0] + '/' + pathParts[1] + + // ...and then remove it from the parts + pathParts.splice(0, 2) + + // Assign Git reference + let ref = pathParts.length >= 2 ? pathParts[1] : '' + + // Shorten SHA for commits + if (pathParts[0] && pathParts[0] === 'commit') { + ref = ref.substring(0, 7) + } + + // We're deploying master by default, + // so there's no need to indicate it explicitly + if (ref === 'master') { + ref = '' + } + + return {main, ref} +} + +export const gitPathParts = main => { + let ref = '' + + if (isURL(main)) { + return splittedURL(main) + } + + if (main.split('/')[1].includes('#')) { + const parts = main.split('#') + + ref = parts[1] + main = parts[0] + } + + return {main, ref} +} + +export const isRepoPath = path => { + if (!path) { + return false + } + + const allowedHosts = [ + 'github.com', + 'gitlab.com' + ] + + if (isURL(path)) { + const urlParts = url.parse(path) + const slashSplitted = urlParts.path.split('/').filter(n => n) + const notBare = slashSplitted.length >= 2 + + if (allowedHosts.includes(urlParts.host) && notBare) { + return true + } + + return 'no-valid-url' + } + + return /[^\s\\]\/[^\s\\]/g.test(path) +} + +export const onGitHub = async (path, debug) => { + let tmpDir = false + + try { + tmpDir = await downloadRepo(path) + } catch (err) { + if (debug) { + console.log(`Could not download "${path}" repo from GitHub`) + } + } + + return tmpDir +} diff --git a/package.json b/package.json index 3590acb..498b088 100644 --- a/package.json +++ b/package.json @@ -74,6 +74,7 @@ "copy-paste": "1.3.0", "cross-spawn": "5.0.1", "docker-file-parser": "0.1.0", + "download": "5.0.2", "email-prompt": "0.1.8", "email-validator": "1.0.7", "fs-promise": "1.0.0", @@ -81,6 +82,7 @@ "graceful-fs": "4.1.11", "ignore": "3.2.0", "ini": "1.3.4", + "is-url": "1.2.2", "minimist": "1.2.0", "ms": "0.7.2", "node-fetch": "1.6.3", @@ -90,7 +92,8 @@ "socket.io-client": "1.7.1", "spdy": "3.4.4", "split-array": "1.0.1", - "text-table": "0.2.0" + "text-table": "0.2.0", + "tmp-promise": "1.0.2" }, "devDependencies": { "alpha-sort": "1.0.2",