import * as Command from 'ember-cli/lib/models/command'; import * as SilentError from 'silent-error'; import { exec } from 'child_process'; import * as Promise from 'ember-cli/lib/ext/promise'; import * as chalk from 'chalk'; import * as fs from 'fs'; import * as fse from 'fs-extra'; import * as path from 'path'; import * as WebpackBuild from '../tasks/build-webpack'; import * as CreateGithubRepo from '../tasks/create-github-repo'; import { CliConfig } from '../models/config'; const fsReadFile = Promise.denodeify(fs.readFile); const fsWriteFile = Promise.denodeify(fs.writeFile); const fsReadDir = Promise.denodeify(fs.readdir); const fsCopy = Promise.denodeify(fse.copy); module.exports = Command.extend({ name: 'github-pages:deploy', aliases: ['gh-pages:deploy'], description: 'Build the test app for production, commit it into a git branch, setup GitHub repo and push to it', works: 'insideProject', availableOptions: [ { name: 'message', type: String, default: 'new gh-pages version', description: 'The commit message to include with the build, must be wrapped in quotes.' }, { name: 'target', type: String, default: 'production', aliases: ['t', { 'dev': 'development' }, { 'prod': 'production' }] }, { name: 'environment', type: String, default: '', aliases: ['e'] }, { name: 'user-page', type: Boolean, default: false, description: 'Deploy as a user/org page' }, { name: 'skip-build', type: Boolean, default: false, description: 'Skip building the project before deploying' }, { name: 'gh-token', type: String, default: '', description: 'Github token' }, { name: 'gh-username', type: String, default: '', description: 'Github username' }], run: function(options, rawArgs) { var ui = this.ui; var root = this.project.root; var execOptions = { cwd: root }; if (options.environment === ''){ if (options.target === 'development') { options.environment = 'dev'; } if (options.target === 'production') { options.environment = 'prod'; } } var projectName = this.project.pkg.name; const outDir = CliConfig.fromProject().apps[0].outDir; let ghPagesBranch = 'gh-pages'; let destinationBranch = options.userPage ? 'master' : ghPagesBranch; let initialBranch; // declared here so that tests can stub exec const execPromise = Promise.denodeify(exec); var buildTask = new WebpackBuild({ ui: this.ui, analytics: this.analytics, cliProject: this.project, target: options.target, environment: options.environment, outputPath: outDir }); var buildOptions = { target: options.target, environment: options.environment, outputPath: outDir }; var createGithubRepoTask = new CreateGithubRepo({ ui: this.ui, analytics: this.analytics, project: this.project }); var createGithubRepoOptions = { projectName, ghUsername: options.ghUsername, ghToken: options.ghToken }; return checkForPendingChanges() .then(build) .then(saveStartingBranchName) .then(createGitHubRepoIfNeeded) .then(checkoutGhPages) .then(copyFiles) .then(updateBaseHref) .then(addAndCommit) .then(returnStartingBranch) .then(pushToGitRepo) .then(printProjectUrl) .catch(failGracefully); function checkForPendingChanges() { return execPromise('git status --porcelain') .then(stdout => { if (/\w+/m.test(stdout)) { let msg = 'Uncommitted file changes found! Please commit all changes before deploying.'; return Promise.reject(new SilentError(msg)); } }); } function build() { if (options.skipBuild) return Promise.resolve(); return buildTask.run(buildOptions); } function saveStartingBranchName() { return execPromise('git rev-parse --abbrev-ref HEAD') .then((stdout) => initialBranch = stdout.replace(/\s/g, '')); } function createGitHubRepoIfNeeded() { return execPromise('git remote -v') .then(function(stdout) { if (!/origin\s+(https:\/\/|git@)github\.com/m.test(stdout)) { return createGithubRepoTask.run(createGithubRepoOptions) .then(() => { // only push starting branch if it's not the destinationBranch // this happens commonly when using github user pages, since // they require the destination branch to be 'master' if (destinationBranch !== initialBranch) { execPromise(`git push -u origin ${initialBranch}`); } }); } }); } function checkoutGhPages() { return execPromise(`git checkout ${ghPagesBranch}`) .catch(createGhPagesBranch) } function createGhPagesBranch() { return execPromise(`git checkout --orphan ${ghPagesBranch}`) .then(() => execPromise('git rm --cached -r .', execOptions)) .then(() => execPromise('git add .gitignore', execOptions)) .then(() => execPromise('git clean -f -d', execOptions)) .then(() => execPromise(`git commit -m \"initial ${ghPagesBranch} commit\"`)); } function copyFiles() { return fsReadDir(outDir) .then((files) => Promise.all(files.map((file) => { if (file === '.gitignore'){ // don't overwrite the .gitignore file return Promise.resolve(); } return fsCopy(path.join(outDir, file), path.join('.', file)) }))); } function updateBaseHref() { if (options.userPage) return Promise.resolve(); let indexHtml = path.join(root, 'index.html'); return fsReadFile(indexHtml, 'utf8') .then((data) => data.replace(//g, ``)) .then((data) => { fsWriteFile(indexHtml, data, 'utf8'); fsWriteFile(path.join(root, '404.html'), data, 'utf8'); }); } function addAndCommit() { return execPromise('git add .', execOptions) .then(() => execPromise(`git commit -m "${options.message}"`)) .catch(() => Promise.reject(new SilentError('No changes found. Deployment skipped.'))); } function returnStartingBranch() { return execPromise(`git checkout ${initialBranch}`); } function pushToGitRepo() { return execPromise(`git push origin ${ghPagesBranch}:${destinationBranch}`); } function printProjectUrl() { return execPromise('git remote -v') .then((stdout) => { let userName = stdout.match(/origin\s+(?:https:\/\/|git@)github\.com(?:\:|\/)([^\/]+)/m)[1].toLowerCase(); let url = `https://${userName}.github.io/${options.userPage ? '' : (projectName + '/')}`; ui.writeLine(chalk.green(`Deployed! Visit ${url}`)); ui.writeLine('Github pages might take a few minutes to show the deployed site.'); }); } function failGracefully(error) { if (error && (/git clean/.test(error.message) || /Permission denied/.test(error.message))) { ui.writeLine(error.message); let msg = 'There was a permissions error during git file operations, please close any open project files/folders and try again.'; msg += `\nYou might also need to return to the ${initialBranch} branch manually.`; return Promise.reject(new SilentError(msg)); } else { return Promise.reject(error); } } } });