diff --git a/README.md b/README.md
index 749147bfad21..a7270a228223 100644
--- a/README.md
+++ b/README.md
@@ -123,7 +123,7 @@ with two sub-routes. The file structure will be as follows:
...
```
-By default the cli will add the import statements for HeroList and HeroDetail to
+By default the cli will add the import statements for HeroList and HeroDetail to
`hero-root.component.ts`:
```
@@ -148,7 +148,7 @@ export const CliRouteConfig = [
Visiting `http://localhost:4200/hero` will show the hero list.
-There is an optional flag for `skip-router-generation` which will not add the route to the `CliRouteConfig` for the application.
+There is an optional flag for `skip-router-generation` which will not add the route to the `CliRouteConfig` for the application.
### Creating a build
@@ -166,7 +166,7 @@ ng test
Tests will execute after a build is executed via [Karma](http://karma-runner.github.io/0.13/index.html)
-If run with the watch argument `--watch` (shorthand `-w`) builds will run when source files have changed
+If run with the watch argument `--watch` (shorthand `-w`) builds will run when source files have changed
and tests will run after each successful build
@@ -183,16 +183,25 @@ End-to-end tests are ran via [Protractor](https://angular.github.io/protractor/)
### Deploying the app via GitHub Pages
-The CLI currently comes bundled with [angular-cli-github-pages addon](https://github.com/IgorMinar/angular-cli-github-pages).
-
-This means that you can deploy your apps quickly via:
+You can deploy your apps quickly via:
```
-git commit -a -m "final tweaks before deployment - what could go wrong?"
-ng github-pages:deploy
+ng github-pages:deploy --message "Optional commit message"
```
-Checkout [angular-cli-github-pages addon](https://github.com/IgorMinar/angular-cli-github-pages) docs for more info.
+This will do the following:
+
+- creates GitHub repo for the current project if one doesn't exist
+- rebuilds the app at the current `HEAD`
+- creates a local `gh-pages` branch if one doesn't exist
+- moves your app to the `gh-pages` branch and creates a commit
+- edit the base tag in index.html to support github pages
+- pushes the `gh-pages` branch to github
+- returns back to the original `HEAD`
+
+Creating the repo requires a token from github, and the remaining functionality
+relies on ssh authentication for all git operations that communicate with github.com.
+To simplify the authentication, be sure to [setup your ssh keys](https://help.github.com/articles/generating-ssh-keys/).
### Linting and formatting code
@@ -201,15 +210,6 @@ This will use the `lint`/`format` npm script that in generated projects uses `ts
You can modify the these scripts in `package.json` to run whatever tool you prefer.
-
-### Formatting code
-
-You can format your app code by running `ng format`.
-This will use the `format` npm script that in generated projects uses `clang-format`.
-
-You can modify the `format` script in `package.json` to run whatever formatting tool
-you prefer and `ng format` will still run it.
-
### Support for offline applications
By default a file `manifest.appcache` will be generated which lists all files included in
diff --git a/addon/ng2/blueprints/ng2/files/package.json b/addon/ng2/blueprints/ng2/files/package.json
index 18cd77f60041..78276eb02e1a 100644
--- a/addon/ng2/blueprints/ng2/files/package.json
+++ b/addon/ng2/blueprints/ng2/files/package.json
@@ -22,7 +22,6 @@
},
"devDependencies": {
"angular-cli": "0.0.*",
- "angular-cli-github-pages": "^0.2.0",
"clang-format": "^1.0.35",
"codelyzer": "0.0.12",
"ember-cli-inject-live-reload": "^1.4.0",
diff --git a/addon/ng2/commands/github-pages-deploy.ts b/addon/ng2/commands/github-pages-deploy.ts
new file mode 100644
index 000000000000..76b036f04e15
--- /dev/null
+++ b/addon/ng2/commands/github-pages-deploy.ts
@@ -0,0 +1,184 @@
+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 BuildTask from 'ember-cli/lib/tasks/build';
+import * as win from 'ember-cli/lib/utilities/windows-admin';
+import * as CreateGithubRepo from '../tasks/create-github-repo';
+
+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: 'environment',
+ type: String,
+ default: 'production',
+ description: 'The Angular environment to create a build for'
+ }, {
+ name: 'branch',
+ type: String,
+ default: 'gh-pages',
+ description: 'The git branch to push your pages to'
+ }, {
+ 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
+ };
+ var projectName = this.project.pkg.name;
+
+ let initialBranch;
+
+ // declared here so that tests can stub exec
+ const execPromise = Promise.denodeify(exec);
+
+ var buildTask = new BuildTask({
+ ui: this.ui,
+ analytics: this.analytics,
+ project: this.project
+ });
+
+ var buildOptions = {
+ environment: options.environment,
+ outputPath: 'dist/'
+ };
+
+ 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);
+
+ 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 win.checkWindowsElevation(ui)
+ .then(() => buildTask.run(buildOptions));
+ }
+
+ function saveStartingBranchName() {
+ return execPromise('git rev-parse --abbrev-ref HEAD')
+ .then((stdout) => initialBranch = stdout);
+ }
+
+ function createGitHubRepoIfNeeded() {
+ return execPromise('git remote -v')
+ .then(function(stdout) {
+ if (!/origin\s+(https:\/\/|git@)github\.com/m.test(stdout)) {
+ return createGithubRepoTask.run(createGithubRepoOptions);
+ }
+ });
+ }
+
+ function checkoutGhPages() {
+ return execPromise(`git checkout ${options.branch}`)
+ .catch(createGhPagesBranch)
+ }
+
+ function createGhPagesBranch() {
+ return execPromise(`git checkout --orphan ${options.branch}`)
+ .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 ${options.branch} commit\"`));
+ }
+
+ function copyFiles() {
+ return fsReadDir('dist')
+ .then((files) => Promise.all(files.map((file) => fsCopy(path.join('dist', file), path.join('.', file)))))
+ }
+
+ function updateBaseHref() {
+ let indexHtml = path.join(root, 'index.html');
+ return fsReadFile(indexHtml, 'utf8')
+ .then((data) => data.replace(//g, ` fsWriteFile(indexHtml, 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(committed) {
+ return execPromise(`git push origin ${options.branch}`);
+ }
+
+ function printProjectUrl() {
+ return execPromise('git remote -v')
+ .then((stdout) => {
+
+ let userName = stdout.match(/origin\s+(?:https:\/\/|git@)github\.com(?:\:|\/)([^\/]+)/m)[1].toLowerCase();
+ ui.writeLine(chalk.green(`Deployed! Visit https://${userName}.github.io/${projectName}/`));
+ ui.writeLine('Github pages might take a few minutes to show the deployed site.');
+ });
+ }
+ }
+});
diff --git a/addon/ng2/index.js b/addon/ng2/index.js
index d9c98fd81adc..54a87033aacb 100644
--- a/addon/ng2/index.js
+++ b/addon/ng2/index.js
@@ -14,7 +14,8 @@ module.exports = {
'format': require('./commands/format'),
'version': require('./commands/version'),
'completion': require('./commands/completion'),
- 'doc': require('./commands/doc')
+ 'doc': require('./commands/doc'),
+ 'github-pages-deploy': require('./commands/github-pages-deploy')
};
}
};
diff --git a/addon/ng2/tasks/create-github-repo.ts b/addon/ng2/tasks/create-github-repo.ts
new file mode 100644
index 000000000000..046c65694e88
--- /dev/null
+++ b/addon/ng2/tasks/create-github-repo.ts
@@ -0,0 +1,77 @@
+import * as Promise from 'ember-cli/lib/ext/promise';
+import * as Task from 'ember-cli/lib/models/task';
+import * as SilentError from 'silent-error';
+import { exec } from 'child_process';
+import * as https from 'https';
+
+module.exports = Task.extend({
+ run: function(commandOptions) {
+ var ui = this.ui;
+ let promise;
+
+ // declared here so that tests can stub exec
+ const execPromise = Promise.denodeify(exec);
+
+ if (/.+/.test(commandOptions.ghToken) && /\w+/.test(commandOptions.ghUsername)) {
+ promise = Promise.resolve({
+ ghToken: commandOptions.ghToken,
+ ghUsername: commandOptions.ghUsername
+ });
+ } else {
+ ui.writeLine("\nIn order to deploy this project via GitHub Pages, we must first create a repository for it.");
+ ui.writeLine("It's safer to use a token than to use a password, so you will need to create one.\n");
+ ui.writeLine("Go to the following page and click 'Generate new token'.");
+ ui.writeLine("https://github.com/settings/tokens\n");
+ ui.writeLine("Choose 'public_repo' as scope and then click 'Generate token'.\n");
+ promise = ui.prompt([
+ {
+ name: 'ghToken',
+ type: 'input',
+ message: 'Please enter GitHub token you just created (used only once to create the repo):',
+ validate: function(token) {
+ return /.+/.test(token);
+ }
+ }, {
+ name: 'ghUsername',
+ type: 'input',
+ message: 'and your GitHub user name:',
+ validate: function(userName) {
+ return /\w+/.test(userName);
+ }
+ }]);
+ }
+
+ return promise
+ .then((answers) => {
+ return new Promise(function(resolve, reject) {
+ var postData = JSON.stringify({
+ 'name': commandOptions.projectName
+ });
+
+ var req = https.request({
+ hostname: 'api.github.com',
+ port: 443,
+ path: '/user/repos',
+ method: 'POST',
+ headers: {
+ 'Authorization': `token ${answers.ghToken}`,
+ 'Content-Type': 'application/json',
+ 'Content-Length': postData.length,
+ 'User-Agent': 'angular-cli-github-pages'
+ }
+ });
+
+ req.on('response', function(response) {
+ if (response.statusCode === 201) {
+ resolve(execPromise(`git remote add origin git@github.com:${answers.ghUsername}/${commandOptions.projectName}.git`))
+ } else {
+ reject(new SilentError(`Failed to create GitHub repo. Error: ${response.statusCode} ${response.statusMessage}`));
+ }
+ });
+
+ req.write(postData);
+ req.end();
+ });
+ });
+ }
+});
diff --git a/package.json b/package.json
index 51ede5aca5f1..1c5a1c858328 100644
--- a/package.json
+++ b/package.json
@@ -67,6 +67,7 @@
"mocha": "^2.4.5",
"object-assign": "^4.0.1",
"rewire": "^2.5.1",
+ "sinon": "^1.17.3",
"through": "^2.3.8",
"tslint": "^3.6.0",
"walk-sync": "^0.2.6"
diff --git a/tests/acceptance/github-pages-deploy.spec.js b/tests/acceptance/github-pages-deploy.spec.js
new file mode 100644
index 000000000000..09bdc06dfc44
--- /dev/null
+++ b/tests/acceptance/github-pages-deploy.spec.js
@@ -0,0 +1,232 @@
+/*eslint-disable no-console */
+'use strict';
+
+var ng = require('../helpers/ng');
+var tmp = require('../helpers/tmp');
+var conf = require('ember-cli/tests/helpers/conf');
+var Promise = require('ember-cli/lib/ext/promise');
+var fs = require('fs');
+var path = require('path');
+var chai = require('chai');
+var sinon = require('sinon');
+var ExecStub = require('../helpers/exec-stub');
+var https = require('https');
+
+const expect = chai.expect;
+const fsReadFile = Promise.denodeify(fs.readFile);
+const fsWriteFile = Promise.denodeify(fs.writeFile);
+const fsMkdir = Promise.denodeify(fs.mkdir);
+
+describe('Acceptance: ng github-pages:deploy', function() {
+ let execStub;
+ let project = 'foo',
+ initialBranch = 'master',
+ branch = 'gh-pages',
+ message = 'new gh-pages version',
+ remote = 'origin git@github.com:username/project.git (fetch)';
+
+ function setupDist() {
+ return fsMkdir('./dist')
+ .then(() => {
+ let indexHtml = path.join(process.cwd(), 'dist', 'index.html');
+ let indexData = `
project\n`;
+ return fsWriteFile(indexHtml, indexData, 'utf8');
+ });
+ }
+
+ before(conf.setup);
+
+ after(conf.restore);
+
+ beforeEach(function() {
+ this.timeout(10000);
+ return tmp.setup('./tmp')
+ .then(() => process.chdir('./tmp'))
+ .then(() => ng(['new', project, '--skip-npm', '--skip-bower']))
+ .then(() => setupDist())
+ .then(() => execStub = new ExecStub());
+ });
+
+ afterEach(function() {
+ this.timeout(10000);
+ return tmp.teardown('./tmp')
+ .then(() => expect(execStub.hasFailed()).to.be.false)
+ .then(() => expect(execStub.hasEmptyStack()).to.be.true)
+ .then(() => execStub.restore());
+ });
+
+ it('should fail with uncommited changes', function() {
+ execStub.addExecSuccess('git status --porcelain', 'M dir/file.ext');
+ return ng(['github-pages:deploy', '--skip-build'])
+ .then((ret) => expect(ret).to.equal(1))
+ });
+
+ it('should deploy with defaults to existing remote', function() {
+ execStub.addExecSuccess('git status --porcelain')
+ .addExecSuccess('git rev-parse --abbrev-ref HEAD', initialBranch)
+ .addExecSuccess('git remote -v', remote)
+ .addExecSuccess(`git checkout ${branch}`)
+ .addExecSuccess('git add .')
+ .addExecSuccess(`git commit -m "${message}"`)
+ .addExecSuccess(`git checkout ${initialBranch}`)
+ .addExecSuccess(`git push origin ${branch}`)
+ .addExecSuccess('git remote -v', remote);
+
+ return ng(['github-pages:deploy', '--skip-build'])
+ .then(() => {
+ let indexHtml = path.join(process.cwd(), 'index.html');
+ return fsReadFile(indexHtml, 'utf8');
+ })
+ .then((data) => expect(data.search(` {
+ let indexHtml = path.join(process.cwd(), 'index.html');
+ return fsReadFile(indexHtml, 'utf8');
+ })
+ .then((data) => expect(data.search(` {
+ let indexHtml = path.join(process.cwd(), 'index.html');
+ return fsReadFile(indexHtml, 'utf8');
+ })
+ .then((data) => expect(data.search(` responseCb = cb,
+ write: (postData) => expect(postData).to.eql(expectedPostData),
+ end: () => responseCb({ statusCode: 201 })
+ }
+ }
+
+ return ng(['github-pages:deploy', '--skip-build', `--gh-token=${token}`,
+ `--gh-username=${username}`])
+ .then(() => {
+ let indexHtml = path.join(process.cwd(), 'index.html');
+ return fsReadFile(indexHtml, 'utf8');
+ })
+ .then((data) => expect(data.search(` httpsStub.restore());
+ });
+
+ it('should stop deploy if create branch fails', function() {
+ let noRemote = '',
+ token = 'token',
+ username = 'username';
+
+ execStub.addExecSuccess('git status --porcelain')
+ .addExecSuccess('git rev-parse --abbrev-ref HEAD', initialBranch)
+ .addExecSuccess('git remote -v', noRemote);
+
+ var httpsStub = sinon.stub(https, 'request', httpsRequestStubFunc);
+
+ function httpsRequestStubFunc(req) {
+ let responseCb;
+
+ let expectedPostData = JSON.stringify({
+ 'name': project
+ });
+
+ let expectedReq = {
+ hostname: 'api.github.com',
+ port: 443,
+ path: '/user/repos',
+ method: 'POST',
+ headers: {
+ 'Authorization': `token ${token}`,
+ 'Content-Type': 'application/json',
+ 'Content-Length': expectedPostData.length,
+ 'User-Agent': 'angular-cli-github-pages'
+ }
+ }
+
+ expect(req).to.eql(expectedReq);
+
+ return {
+ on: (event, cb) => responseCb = cb,
+ write: (postData) => expect(postData).to.eql(expectedPostData),
+ end: () => responseCb({ statusCode: 401, statusMessage: 'Unauthorized' })
+ }
+ }
+
+ return ng(['github-pages:deploy', '--skip-build', `--gh-token=${token}`,
+ `--gh-username=${username}`])
+ .then((ret) => expect(ret).to.equal(1))
+ .then(() => httpsStub.restore());
+ });
+});
diff --git a/tests/helpers/exec-stub.js b/tests/helpers/exec-stub.js
new file mode 100644
index 000000000000..42e7f0e8ec46
--- /dev/null
+++ b/tests/helpers/exec-stub.js
@@ -0,0 +1,71 @@
+'use strict';
+var child_process = require('child_process');
+var sinon = require('sinon');
+
+class ExecStub {
+ constructor() {
+ this.execOrig = child_process.exec;
+ this.stub = sinon.stub(child_process, 'exec', this.execStubFunc.bind(this));
+ this.stack = [];
+ this.failed = false;
+ }
+ execStubFunc(cmd) {
+ let resp;
+
+ // console.log('####running', cmd);
+
+ if (this.failed) {
+ resp = this.failedExec('ExecStub - in fail mode');
+ return resp.apply(null, arguments);
+ }
+
+ if (this.stack.length === 0) {
+ this.failed = true;
+ resp = this.failedExec('ExecStub - expected stack size exceeded');
+ return resp.apply(null, arguments);
+ }
+
+ let item = this.stack.shift();
+
+ // console.log('####expected', item.cmd);
+
+ if (cmd !== item.cmd) {
+ this.failed = true;
+ resp = this.failedExec(`ExecStub - unexpected command: ${cmd}`);
+ return resp.apply(null, arguments);
+ }
+
+ return item.resp.apply(null, arguments);
+ }
+ hasFailed() {
+ return this.failed;
+ }
+ hasEmptyStack() {
+ return this.stack.length === 0;
+ }
+ restore() {
+ this.stub.restore();
+ return this;
+ }
+ addExecSuccess(cmd, sdout) {
+ sdout = sdout || '';
+ this.stack.push({
+ cmd,
+ resp: (cmd, opt, cb) => (cb ? cb : opt)(null, sdout, null)
+ });
+ return this;
+ }
+ addExecError(cmd, stderr) {
+ stderr = stderr || '';
+ this.stack.push({
+ cmd,
+ resp: (cmd, opt, cb) => (cb ? cb : opt)(new Error(stderr), null, stderr)
+ });
+ return this;
+ }
+ failedExec(reason) {
+ return (cmd, opt, cb) => (cb ? cb : opt)(new Error(reason), null, reason)
+ }
+}
+
+module.exports = ExecStub;