Skip to content

Commit

Permalink
feat(circleci): Add CircleCi generator handling all environments
Browse files Browse the repository at this point in the history
Get a state-of-the art CircleCI deployment script for RN provided you ran fastlane-setup and
fastlane-env answering yes to the deployment + secret handling scripts
  • Loading branch information
felixmeziere committed Apr 19, 2019
1 parent 1ed556c commit b103be3
Show file tree
Hide file tree
Showing 10 changed files with 451 additions and 40 deletions.
3 changes: 3 additions & 0 deletions generators/circleci/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# CircleCI setup for continuous integration

CircleCI is a widely used CI service for both web and mobile projects. After running this generator, pushing your code to CircleCI should result in a succesful build configured with dependency caching and good practices.
174 changes: 174 additions & 0 deletions generators/circleci/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
const Base = require('yeoman-generator');
require('colors');
const glob = require('glob');

// Command creators
const getPassphraseAliasForEnvironment = environment =>
`${environment.envName.toUpperCase()}_SECRETS_PASSPHRASE`;
const getUnpackCommandForEnvironment = environment =>
`sudo yarn unpack-secrets -e ${
environment.envName
} -p \${${getPassphraseAliasForEnvironment(environment)}}`;
const getCodePushCommandForEnvironment = environment =>
`yarn deploy -t soft -e ${environment.envName}`;
const getAndroidHardDeployCommandForEnvironment = environment =>
`yarn deploy -t hard -o android -e ${environment.envName}`;
const getIosHardDeployCommandForEnvironment = environment =>
`yarn deploy -t hard -o ios -e ${environment.envName}`;

class CircleGenerator extends Base {
initializing() {
this.composeWith('rn-toolbox:checkversion');
if (!this.config.get('fastlane'))
this.log.error(
'You need to run `yo rn-toolbox:fastlane` first.'.red.bold
);

if (!this.config.get('circleci-ready'))
this.log.error(
'You need to have the deployment script and secrets archive from fastlane-setup to use this generator. Get them by running yo rn-toolbox:fastlane-setup.'
.red.bold
);
}

prompting() {
const config = this.fs.readJSON(this.destinationPath('package.json'));
const envFilepaths = glob.sync('fastlane/.env.*', {
ignore: 'fastlane/.env.*.*',
});
const environments = envFilepaths.map(filePath => {
const split = filePath.split('/');
const envName = split[split.length - 1].split('.')[2];
const fileString = this.fs.read(filePath);
const envGitBranch = fileString
.split("REPO_GIT_BRANCH='")[1]
.split("'")[0];
return {
envName,
envGitBranch,
};
});
if (environments.length === 0)
this.log.error(
'You need at least one environment setup with fastlane-env to run this generator. Run yo rn-toolbox:fastlane-env.'
.red.bold
);

const prompts = [
{
type: 'input',
name: 'projectName',
message:
'Please confirm the react-native project name (as in react-native-init <PROJECT_NAME>)',
required: true,
default: config.name,
},
{
type: 'input',
name: 'reactNativeDirectory',
message:
'Path to the React Native project relative to the root of the repository (no trailing slash)',
required: true,
default: '.',
},
];

return this.prompt(prompts).then(answers => {
this.answers = answers;
this.environments = environments;
});
}

writing() {
// Command creators
const numberOfEnvironments = this.environments.length;
const getPerEnvironmentCommand = commandGetter => {
let switchString = '';
this.environments.forEach((environment, index) => {
const prefix = index === 0 ? 'if' : 'elif';
const suffix = index === numberOfEnvironments - 1 ? 'fi' : '';
if (index > 0) switchString += '';
switchString += `${prefix} [ "\${CIRCLE_BRANCH}" == "${
environment.envGitBranch
}" ];
then
${commandGetter(environment)}
${suffix}`;
});
return switchString;
};
const getForAllEnvironmentsCommand = command => {
let ifStatement = `if [ "\${CIRCLE_BRANCH}" == "${
this.environments[0].envGitBranch
}" ]`;
this.environments.slice(1).forEach(environment => {
ifStatement += `|| [ "\${CIRCLE_BRANCH}" == "${environment.envName}" ]`;
});
ifStatement += ';';

return `${ifStatement}
then
${command}
fi`;
};

// Commands
const unpackSecretsCommand = getPerEnvironmentCommand(
getUnpackCommandForEnvironment
);
const installAppcenterCommand = getForAllEnvironmentsCommand(`echo 'export PATH=$(yarn global bin):$PATH' >> $BASH_ENV
source $BASH_ENV
yarn global add appcenter-cli
appcenter login --token \${FL_APPCENTER_API_TOKEN} --quiet`);
const codepushCommand = getPerEnvironmentCommand(
getCodePushCommandForEnvironment
);
const androidHardDeployCommand = getPerEnvironmentCommand(
getAndroidHardDeployCommandForEnvironment
);
const iosHardDeployCommand = getPerEnvironmentCommand(
getIosHardDeployCommandForEnvironment
);
let branchesOnlyCommand = '';
this.environments.forEach(environment => {
branchesOnlyCommand += `
- ${environment.envGitBranch}`;
});

// Create final config.yml file :-)
this.fs.copyTpl(
this.templatePath('config.yml'),
this.destinationPath('.circleci/config.yml'),
{
...this.answers,
prefixRN: path => `${this.answers.reactNativeDirectory}/${path || ''}`,
unpackSecretsCommand,
installAppcenterCommand,
codepushCommand,
androidHardDeployCommand,
iosHardDeployCommand,
branchesOnlyCommand,
}
);
}

end() {
this.log(
`Custom config.yml created. Re-run yo rn-toolbox:circleci anytime to re-generate the file with latest environments information.`
.green.bold
);
this.log(
`Please make sure that all of the following environment variables have been added in the Circle-CI console's Environment Variables section:`
.magenta.bold
);
this.log(
['FL_APPCENTER_API_TOKEN', 'MATCH_PASSWORD']
.concat(this.environments.map(getPassphraseAliasForEnvironment))
.join(', ').magenta.bold
);
this.config.set('circleci', true);
this.config.save();
}
}

module.exports = CircleGenerator;
204 changes: 204 additions & 0 deletions generators/circleci/templates/config.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
# prettier: disable

version: 2
jobs:
node:
working_directory: ~/<%= projectName %>
docker:
- image: circleci/android:api-28-node
steps:
- checkout

- restore_cache:
key: yarn-v1-{{ checksum "<%= prefixRN('yarn.lock') %>" }}-{{ arch }}

- restore_cache:
key: node-v1-{{ checksum "<%= prefixRN('package.json') %>" }}-{{ arch }}

- run:
name: yarn install
command: |
cd <%= prefixRN() %>
yarn
- run:
name: jest tests
command: |
cd <%= prefixRN() %>
mkdir -p test-results/jest
yarn test --maxWorkers=2
environment:
JEST_JUNIT_OUTPUT: <%= prefixRN('test-results/jest/junit.xml') %>

- run:
name: Unpack secrets
command: |
cd <%= prefixRN() %>
<%- unpackSecretsCommand %>
- restore_cache:
key: bundle-v1-{{ checksum "<%= prefixRN('Gemfile.lock') %>" }}-{{ arch }}

- run: cd <%= prefixRN() %> && bundle check --path=vendor/bundle || bundle install --path=vendor/bundle --jobs=4 --retry=3

- deploy:
name: Deploy with CodePush
command: |
cd <%= prefixRN() %>
<%- installAppcenterCommand %>
<%- codepushCommand %>
- save_cache:
key: bundle-v1-{{ checksum "<%= prefixRN('Gemfile.lock') %>" }}-{{ arch }}
paths:
- ~/.bundle/
- vendor/bundle

- save_cache:
key: yarn-v1-{{ checksum "<%= prefixRN('yarn.lock') %>" }}-{{ arch }}
paths:
- ~/.cache/yarn

- save_cache:
key: node-v1-{{ checksum "<%= prefixRN('package.json') %>" }}-{{ arch }}
paths:
- <%= prefixRN('node_modules') %>

- persist_to_workspace:
root: ~/<%= projectName %>
paths:
- <%= prefixRN('node_modules') %>
- vendor/bundle

- store_test_results:
path: <%= prefixRN('test-results') %>

- store_artifacts:
path: <%= prefixRN('test-results') %>

android:
working_directory: ~/<%= projectName %>
docker:
- image: circleci/android:api-28-node
steps:
- checkout:
path: ~/<%= projectName %>

- attach_workspace:
at: ~/<%= projectName %>

- run:
name: Unpack secrets
command: |
cd <%= prefixRN() %>
<%- unpackSecretsCommand %>
- run: cd <%= prefixRN() %> && bundle check --path=vendor/bundle || bundle install --path=vendor/bundle --jobs=4 --retry=3

- run:
name: yarn install
command: |
cd <%= prefixRN() %>
yarn
- deploy:
name: Build, Sign & Deploy
command: |
cd <%= prefixRN() %>
<%- androidHardDeployCommand %>
- store_test_results:
path: <%= prefixRN('test-results') %>

- store_artifacts:
path: <%= prefixRN('test-results') %>

ios:
macos:
xcode: '10.1.0'
environment:
LC_ALL: en_US.UTF-8
LANG: en_US.UTF-8
working_directory: ~/<%= projectName %>

# use a --login shell so our "set Ruby version" command gets picked up for later steps
shell: /bin/bash --login -o pipefail

steps:
- checkout

- run:
name: set Ruby version
command: echo "ruby-2.4" > ~/.ruby-version

- run:
name: Install brew dependencies
command: HOMEBREW_NO_AUTO_UPDATE=1 brew install gpg

- run:
name: Unpack secrets
command: |
cd <%= prefixRN() %>
<%- unpackSecretsCommand %>
- restore_cache:
key: yarn-v1-{{ checksum "<%= prefixRN('yarn.lock') %>" }}-{{ arch }}

- restore_cache:
key: node-v1-{{ checksum "<%= prefixRN('package.json') %>" }}-{{ arch }}

# not using a workspace here as Node and Yarn versions
# differ between the macOS executor image and the Docker containers above
- run:
name: yarn install
command: |
cd <%= prefixRN() %>
yarn
- save_cache:
key: yarn-v1-{{ checksum "<%= prefixRN('yarn.lock') %>" }}-{{ arch }}
paths:
- ~/.cache/yarn

- save_cache:
key: node-v1-{{ checksum "<%= prefixRN('package.json') %>" }}-{{ arch }}
paths:
- <%= prefixRN('node_modules') %>

- restore_cache:
key: bundle-v2-{{ checksum "<%= prefixRN('Gemfile.lock') %>" }}-{{ arch }}

- run:
command: bundle check --path=vendor/bundle || bundle install --path=vendor/bundle --jobs=4 --retry=3

- save_cache:
key: bundle-v2-{{ checksum "<%= prefixRN('Gemfile.lock') %>" }}-{{ arch }}
paths:
- ~/.bundle/
- /Users/distiller/.gem/ruby/2.4.5/
- vendor/bundle

- deploy:
name: Match, Build, Sign & Deploy
command: |
cd <%= prefixRN() %>
sudo xcode-select -s /Applications/Xcode-10.1.app
<%- iosHardDeployCommand %>
workflows:
version: 2
node-android-ios:
jobs:
- node
- ios:
requires:
- node
filters:
branches:
only: <%- branchesOnlyCommand %>
- android:
requires:
- node
filters:
branches:
only: <%- branchesOnlyCommand %>
Loading

0 comments on commit b103be3

Please sign in to comment.