-
Notifications
You must be signed in to change notification settings - Fork 119
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #219 from felixmeziere/circle-ci
feat(circleci): Add CircleCi generator handling all environments
- Loading branch information
Showing
10 changed files
with
451 additions
and
40 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 %> |
Oops, something went wrong.