Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
18deca4
chore: add prettier style as usual for graasp apps
Jul 9, 2020
d248ef0
feat: add basic new command
Jul 9, 2020
70c76dc
feat: load dev env and deploy to aws
Jul 10, 2020
305efa0
refactor: rm test file and rm obsolet promisify()
Jul 10, 2020
53925c3
refactor: remove accidentally added bash script
Jul 10, 2020
68fb33b
feat: add options and validation
Jul 10, 2020
00f48a7
feat: invalidate cache after deployment
Jul 10, 2020
8ba8262
fix: allow console output in eslint
Jul 11, 2020
96d3531
feat: add a fancy progress bar
Jul 11, 2020
7ac7faa
fix: tag validation and accidental async
Jul 11, 2020
dc383ad
chore: add comment to RegExp and backshlashes
Jul 11, 2020
c37afb8
refactor: rename varIsDefined() to isDefined()
Jul 11, 2020
305cb3b
refactor: remove unused env varas from import
Jul 11, 2020
19eb223
refactor: replace unnamed func with arrow func
Jul 11, 2020
820b5f5
fix: minor changes based on feedback on pr
Jul 11, 2020
ad0f6f6
chore: remove obsolete comment
Jul 11, 2020
5687b3b
refactor: factor validation out
Jul 11, 2020
cb5650a
fix: use proper tag validation
Jul 17, 2020
6b38e41
chore: add explanation to regexp
Jul 17, 2020
f4b79ef
fix: remove default env
Jul 17, 2020
3cf0366
refactor: replace isdefined() with lodash
Jul 17, 2020
e85ac36
chore: enable linting warning for console log
Jul 18, 2020
baaa792
Merge branch 'master' of github.com:graasp/graasp-cli into deploy-cmd
Jul 26, 2020
2338ead
chore: try out promisify
Jul 26, 2020
4491092
chore: force commit with eslint errors
Jul 26, 2020
62ea0f8
feat: load sync aws credentials
Jul 26, 2020
89e8821
chore: update flag descriptions
Jul 26, 2020
7a288e8
chore: assemble error outputs in together
Jul 26, 2020
28a0dc5
chore: remove obsolete default env variable
Jul 26, 2020
cfa7135
Merge pull request #45 from graasp/promisify
ugGit Jul 26, 2020
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 3 additions & 5 deletions .eslintrc
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"extends": "airbnb-base",
"extends": ["airbnb-base", "prettier"],
"env": {
"node": true,
"mocha": true
Expand All @@ -9,11 +9,9 @@
"no-underscore-dangle": [
"error",
{
"allow": [
"_id"
]
"allow": ["_id"]
}
],
"import/no-named-as-default": 0
"import/no-named-as-default": "off"
}
}
6 changes: 6 additions & 0 deletions .prettierrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"trailingComma": "all",
"tabWidth": 2,
"semi": true,
"singleQuote": true
}
5 changes: 5 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,21 +48,26 @@
"babel-eslint": "10.1.0",
"eslint": "6.8.0",
"eslint-config-airbnb-base": "14.1.0",
"eslint-config-prettier": "6.11.0",
"eslint-plugin-import": "2.20.1",
"husky": "4.2.3",
"npm-run-all": "4.1.5",
"standard-version": "7.1.0"
},
"dependencies": {
"@babel/polyfill": "7.8.7",
"aws-sdk": "2.713.0",
"bson-objectid": "1.3.0",
"cli-progress": "3.8.2",
"del": "4.1.1",
"dotenv": "8.2.0",
"execa": "1.0.0",
"fs-exists-cached": "1.0.0",
"fs-extra": "7.0.1",
"hosted-git-info": "2.7.1",
"inquirer": "6.2.2",
"lodash": "4.17.19",
"s3-node-client": "4.4.4",
"yargs": "12.0.5"
}
}
4 changes: 4 additions & 0 deletions src/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,7 @@ export const AWS_SECRET_ACCESS_KEY_LENGTH = 40;

// file names
export const GRAASP_IGNORE_FILE = '.graaspignore';

// deploy settings
export const DEFAULT_BUILD_DIR = './build';
export const DEFAULT_APP_VERSION = 'latest';
80 changes: 52 additions & 28 deletions src/createCli.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
import yargs from 'yargs';
import prompt from './prompt';
import { DEFAULT_STARTER } from './config';

const promisify = (fn) => (...args) => {
Promise.resolve(fn(...args)).then(
() => process.exit(0),
// err => report.panic(err)
);
};
import deploy from './deploy';
import {
DEFAULT_STARTER,
DEFAULT_BUILD_DIR,
DEFAULT_APP_VERSION,
} from './config';

const createCli = (argv) => {
const cli = yargs();
Expand All @@ -33,25 +31,52 @@ const createCli = (argv) => {
.command({
command: 'new',
desc: 'Create new Graasp app.',
builder: (_) => _.option('s', {
alias: 'starter',
type: 'string',
default: DEFAULT_STARTER,
describe: `Set starter. Defaults to ${DEFAULT_STARTER}`,
}).option('f', {
alias: 'framework',
type: 'string',
describe: 'Set development framework (e.g. React, Angular)',
}).option('t', {
alias: 'type',
choices: ['app', 'lab'],
describe: 'Type of application (app or lab)',
}).option('p', {
alias: 'path',
type: 'string',
describe: 'Path where project directory will be set up.',
}),
handler: promisify(prompt),
builder: (_) =>
_.option('s', {
alias: 'starter',
type: 'string',
default: DEFAULT_STARTER,
describe: `Set starter. Defaults to ${DEFAULT_STARTER}`,
})
.option('f', {
alias: 'framework',
type: 'string',
describe: 'Set development framework (e.g. React, Angular)',
})
.option('t', {
alias: 'type',
choices: ['app', 'lab'],
describe: 'Type of application (app or lab)',
})
.option('p', {
alias: 'path',
type: 'string',
describe: 'Path where project directory will be set up.',
}),
handler: prompt,
})
.command({
command: 'deploy',
desc: 'Deploy a Graasp app to AWS',
builder: (_) =>
_.option('t', {
alias: 'tag',
type: 'string',
default: DEFAULT_APP_VERSION,
describe: 'Tag the deployment with a version',
})
.option('e', {
alias: 'env',
type: 'string',
describe: 'Environment file used to load variables from',
})
.option('b', {
alias: 'build',
type: 'string',
default: DEFAULT_BUILD_DIR,
describe: 'Path to the build directory that will be deployed',
}),
handler: deploy,
})
.wrap(cli.terminalWidth())
.demandCommand(1, 'Pass --help to see all available commands and options.')
Expand All @@ -61,5 +86,4 @@ const createCli = (argv) => {
.parse(argv.slice(2));
};


export default createCli;
200 changes: 200 additions & 0 deletions src/deploy.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
import aws from 'aws-sdk';
import s3 from 's3-node-client';
import dotenv from 'dotenv';
import fs from 'fs';
import cliProgress from 'cli-progress';
import _ from 'lodash';
// import { promisify } from './utils';

const validateTag = (tag) => {
// Both compilation hints because of backslashes used in RegExp but unecessary by conception in JS Strings
// Furthermore, the escaption is needed so RegExp will interpret the String correctly.
// prettier-ignore
// eslint-disable-next-line no-useless-escape
const pattern = new RegExp('^v(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)(\\-[0-9A-Za-z]*)?$')
if (tag === 'latest' || pattern.test(tag)) {
console.info(`validated tag ${tag}`);
return true;
}
console.error(`unable to validate version '${tag}'`);
return false;
};

const validateEnv = (env) => {
if (fs.existsSync(env)) {
console.info(`validated environment file ${env}`);
return true;
}
console.error(`environment file '${env}' does not exist`);
return false;
};

const validateBuild = (build) => {
if (fs.existsSync(build)) {
console.info(`validated build directory ${build}`);
return true;
}
console.error(`build directory '${build}' does not exist`);
return false;
};

const validateAppVariables = ({
REACT_APP_HOST,
REACT_APP_GRAASP_DEVELOPER_ID,
REACT_APP_GRAASP_APP_ID,
}) => {
if (
_.isUndefined(REACT_APP_HOST) ||
_.isUndefined(REACT_APP_GRAASP_DEVELOPER_ID) ||
_.isUndefined(REACT_APP_GRAASP_APP_ID)
) {
console.error(
`environment variables REACT_APP_GRAASP_APP_ID, REACT_APP_GRAASP_DEVELOPER_ID and/or REACT_APP_HOST are not defined \n
you can specify them through a .env file in the app root folder or through another file specified with the -e flag`,
);
return false;
}
return true;
};

const validateAwsCredentialsVariables = ({
BUCKET,
AWS_ACCESS_KEY_ID,
AWS_SECRET_ACCESS_KEY,
}) => {
if (
_.isUndefined(BUCKET) ||
_.isUndefined(AWS_ACCESS_KEY_ID) ||
_.isUndefined(AWS_SECRET_ACCESS_KEY)
) {
console.error(
`environment variables BUCKET, AWS_ACCESS_KEY_ID and/or AWS_SECRET_ACCESS_KEY are not defined. \n
make sure you setup your credentials file correctly using the scripts/setup.sh script and contact your favourite Graasp engineer if you keep running into trouble`,
);
return false;
}
return true;
};

const loadAwsCredentials = async () => {
const awsCredentials = new aws.Credentials();
await awsCredentials.getPromise().then(
async () => true,
(err) => {
console.error(err.stack);
return false;
},
);
// set the AWS credentials into the global object
aws.config.credentials = awsCredentials;
};

const deploy = async (opts) => {
const { tag, env, build } = opts;

// validate command options
if (!validateTag(tag) || !validateEnv(env) || !validateBuild(build)) {
console.error('aborting deployment...');
return false;
}

// load environment variables
dotenv.config({ path: env });

// validate environment variables
if (
!validateAppVariables(process.env) ||
!validateAwsCredentialsVariables(process.env)
) {
return false;
}

const {
REACT_APP_GRAASP_DEVELOPER_ID,
REACT_APP_GRAASP_APP_ID,
REACT_APP_HOST,
REACT_APP_VERSION,
BUCKET,
DISTRIBUTION,
} = process.env;

console.info(
`publishing app ${REACT_APP_GRAASP_APP_ID} version ${REACT_APP_VERSION}`,
);

loadAwsCredentials();

const APP_PATH = `${REACT_APP_GRAASP_DEVELOPER_ID}/${REACT_APP_GRAASP_APP_ID}/${REACT_APP_VERSION}`;

const client = s3.createClient({ s3Client: new aws.S3() });

const params = {
localDir: build,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did not see comment on left side for the delete removed comments:

Put both lines of comment above deleteRemoved.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@juancarlosfarah Please let me know if here is still something pending

deleteRemoved: true, // default false, whether to remove s3 objects
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Put both lines of comment above deleteRemoved.

// that have no corresponding local file.

s3Params: {
Bucket: BUCKET,
Prefix: APP_PATH,
// other options supported by putObject, except Body and ContentLength.
// See: http://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/S3.html#putObject-property
},
};
const progressBar = new cliProgress.SingleBar(
{},
cliProgress.Presets.shades_classic,
);
const uploader = client.uploadDir(params);
uploader.on('error', (err) => {
console.error('unable to sync:', err.stack);
});
uploader.on('progress', () => {
progressBar.start(uploader.progressTotal, 0);
progressBar.update(uploader.progressAmount);
});
uploader.on('end', () => {
progressBar.stop();
// TODO: insert here code that should be executed once the upload is done
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be done. You can always create helper functions.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@juancarlosfarah Yes, you are right. But I would require more time to do this. Might consider implementing this when finalizing the deploy command.

// e.g. invalidate cache
});

console.info(
`published app to https://${REACT_APP_HOST}/${APP_PATH}/index.html`,
);

// ensure the correct distribution variables are defined
if (_.isUndefined(DISTRIBUTION)) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

everything after line 159 should be inside the on('end', () => { call in order to work. Preferably you can use the promises version of the native s3 client from AWS.

console.error('environment variable DISTRIBUTION is not defined');
console.error(
'contact your favourite Graasp engineer if you keep running into trouble',
);
return false;
}

// invalidate cloudfront distribution
const pathsToInvalidate = [`/${APP_PATH}/*`];
const invalidationParams = {
DistributionId: DISTRIBUTION,
InvalidationBatch: {
CallerReference: new Date().toString(),
Paths: {
Quantity: pathsToInvalidate.length,
Items: pathsToInvalidate,
},
},
};
const cloudfront = new aws.CloudFront();
cloudfront.createInvalidation(invalidationParams, (err, data) => {
if (err) {
// an error occurred
console.error(err, err.stack);
} else {
// successful response
console.info(data);
}
});

return true;
};

export default deploy;
10 changes: 6 additions & 4 deletions src/initStarter.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import del from 'del';
import { sync as existsSync } from 'fs-exists-cached';
import { DEFAULT_PATH, DEFAULT_STARTER, GRAASP_IGNORE_FILE } from './config';
import writeEnvFiles from './writeEnvFiles';
import { spawn } from './utils';
import { spawnProcess } from './utils';

const readFile = util.promisify(fs.readFile);

Expand All @@ -35,7 +35,9 @@ const install = async (rootPath) => {
process.chdir(rootPath);

try {
const cmd = shouldUseYarn() ? spawn('yarnpkg') : spawn('npm install');
const cmd = shouldUseYarn()
? spawnProcess('yarnpkg')
: spawnProcess('npm install');
await cmd;
} finally {
process.chdir(prevDir);
Expand Down Expand Up @@ -87,7 +89,7 @@ const commit = async (rootPath) => {
process.chdir(rootPath);

try {
await spawn('git add -A', { stdio: 'ignore' });
await spawnProcess('git add -A', { stdio: 'ignore' });

// cannot spawn this because of the way we are splitting the command
execSync('git commit -m "chore: initial commit from graasp cli"', {
Expand Down Expand Up @@ -130,7 +132,7 @@ const clone = async (hostInfo, rootPath) => {

console.log(`creating new site from git: ${url}`);

await spawn(`git clone ${branch} ${url} ${rootPath} --single-branch`);
await spawnProcess(`git clone ${branch} ${url} ${rootPath} --single-branch`);

console.log('created starter directory layout');

Expand Down
Loading