Compare commits

..
4 Commits
Author SHA1 Message Date
Nick Novitski 0da20f4ab5 Instantiate logger only at entrypoints
This ensures buildPhase, jobID, and experienceName labels are propagated when appropriate.
2019-04-11 12:47:06 -07:00
Nick Novitski 05d55bf731 Remove logger.withFields 2019-04-10 16:57:33 -07:00
Nick Novitski 074ba521f9 yarn add '@google-cloud/logging-bunyan@latest' 2019-04-10 16:53:31 -07:00
Nick Novitski 451999f0b9 Search tools ignore fixtures 2019-04-10 16:53:01 -07:00
116 changed files with 2708 additions and 4194 deletions

No files matched your search

+16
View File
@@ -0,0 +1,16 @@
{
"presets": [
["env", {
"targets": {
"node": "8.9"
}
}]
],
"plugins": [
["module-resolver", {
"alias": {
"turtle": "./build"
}
}]
]
}
+5 -75
View File
@@ -3,7 +3,7 @@ version: 2.1
executors:
nix: &nix
docker:
- image: lnl7/nix:2.2
- image: lnl7/nix:2.1.2
# Symlink to the readline-enabled bash which is the default command of the container
shell: /run/current-system/sw/bin/bash -leo pipefail
working_directory: ~/turtle
@@ -33,12 +33,10 @@ commands:
command: |
nix-env -iA \
nixpkgs.binutils \
nixpkgs.curl \
nixpkgs.direnv \
nixpkgs.findutils \
nixpkgs.gawk \
nixpkgs.git \
nixpkgs.gnused \
nixpkgs.gnutar \
nixpkgs.gzip \
nixpkgs.openssh \
@@ -137,31 +135,13 @@ commands:
paths:
- "<< parameters.workingdir >>/<< parameters.platform >>/<< parameters.sdk_version >>"
deploy_turtle:
parameters:
platform:
type: string
env:
type: string
steps:
- checkout
- run:
name: Install curl & jq
command: nix-env -iA nixpkgs.curl nixpkgs.jq
- run:
name: Schedule a Turtle deploy
command: |
echo "We're going to deploy << parameters.platform >> Turtles to << parameters.env >>"
./scripts/deploy.sh "<< parameters.platform >>" "<< parameters.env >>"
jobs:
build:
executor: nix
docker:
- image: circleci/node:8.12
steps:
- setup
- restore_yarn_cache
- checkout
- run: yarn
- save_yarn_cache
- run: yarn lint
- run: yarn build
@@ -173,7 +153,7 @@ jobs:
- run: yarn
- save_yarn_cache
- run:
name: Run unit tests
name: Run iOS smoke tests
command: ./scripts/tests/runUnitTestsCI.sh
ios_smoke_tests:
@@ -220,34 +200,6 @@ jobs:
sdk_version: sdk32
platform: android
android_staging_deploy:
executor: nix
steps:
- deploy_turtle:
platform: android
env: staging
android_production_deploy:
executor: nix
steps:
- deploy_turtle:
platform: android
env: production
ios_staging_deploy:
executor: nix
steps:
- deploy_turtle:
platform: ios
env: staging
ios_production_deploy:
executor: nix
steps:
- deploy_turtle:
platform: ios
env: production
workflows:
checks:
jobs:
@@ -257,25 +209,3 @@ workflows:
- unit_tests
- ios_smoke_tests
- android_smoke_tests
deploy:
jobs:
- android_staging_approve_deploy:
type: approval
- android_staging_deploy:
requires:
- android_staging_approve_deploy
- android_production_approve_deploy:
type: approval
- android_production_deploy:
requires:
- android_production_approve_deploy
- ios_staging_approve_deploy:
type: approval
- ios_staging_deploy:
requires:
- ios_staging_approve_deploy
- ios_production_approve_deploy:
type: approval
- ios_production_deploy:
requires:
- ios_production_approve_deploy
+4 -4
View File
@@ -1,4 +1,4 @@
#!/usr/bin/env bash
#!/bin/bash
source_local() {
file=./.envrc.local
@@ -8,13 +8,13 @@ source_local() {
watch_file $file
}
source_up || true
source_local
export NODE_ENV=development
export EXPO_TURTLE_TOOLS_DIR="$(pwd)/tools"
PATH_add $EXPO_TURTLE_TOOLS_DIR/bin
if [ "$CI" = "true" ]; then
PATH_add $EXPO_TURTLE_TOOLS_DIR/ci/bin
if [ -z "$EXPO_UNIVERSE_DIR" ]; then
PATH_add $EXPO_TURTLE_TOOLS_DIR/bin
fi
-1
View File
@@ -2,7 +2,6 @@
node_modules
yarn-error.log
google-cloud-credentials.json
/build/
/workingdir/*
-1
View File
@@ -1 +0,0 @@
v8.15.1
+3 -89
View File
@@ -5,98 +5,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
## [0.8.7] - 2019-07-05
### Added
- Adhoc Builds: use provisioningProfileId if specified.
- Registering supported SDK versions in Redis.
- Support for 64 bit builds in SDK33.
## [0.8.6] - 2019-06-25
### Added
- Added support for Android build modes: debug or release.
### Fixed
- Upgrading the shell app for a given SDK version when the previous version is already downloaded (bug in `turtle-cli`).
- Updated Android shell app for SDK33 to include `[email protected]` which fixes crashes on Android 9.0 when showing some Lottie animations.
- Schema for `buildModes`.
- Updated @expo/xdl to 55.0.7 (fixed reading from binary plist).
## [0.8.4] - 2019-06-13
### Changed
- Upgraded `sharp` from `0.21.3` to `0.22.1`.
- `gulp-cli` is no longer required to build an app with `turtle-cli`.
## [0.8.3] - 2019-06-13
### Added
- Running Android Turtle process with `supervisor` (pod is restarted if there were 5 failures in the last 10 minutes).
### Changed
- `turtle-cli` is now fetching the app manifest before running the build.
## [0.8.2] - 2019-06-11
### Changed
- Updated Android shell app for SDK33 to remove `expo-face-detector` and `expo-payments-stripe` from standalone builds.
- Updated Android shell app for SDK33 to get rid of `node_modules` for `tools-public`.
- From now on, `turtle-cli` installs dependencies for `tools-public` in shell apps for SDK versions < 33.
### Fixed
- Android builds with turtle-cli by installing all unmodules if `dependencies` is empty.
## [0.8.1] - 2019-06-10
### Changed
- Add expo dependencies when resolving unimodules.
### Fixed
- Support for Android tarballs without `universe-package.json`.
## [0.8.0] - 2019-06-07
### Added
- Turtle CLI logo to the project (created by [@zularizal](https://github.com/zularizal))!
- Shell apps for SDK 33.
- Support for Android App Bundles (App bundles are built by default, use `-t apk` to build apk).
### Changed
- Updated Android shell app for SDK 32 to fix AppAuth issue (https://github.com/expo/expo/pull/4115) and to fix a newer than expected JSC version.
- Default value for build timeout to 15 minutes.
- Added filtering of logs (strips out download progress) when downloading SDK tools.
- `xdl` -> `@expo/xdl`, updated `@expo/xdl`.
### Fixed
- Generating Android keystore in turtle when one is not passed to turtle-cli (https://github.com/expo/turtle/pull/65).
- Upload logs to s3 if build has been canceled or timed out.
- Unhandled exception on asset download (xdl).
## [0.7.2] - 2019-05-09
### Added
- Support for Android tarballs without `universe-package.json`.
### Changed
- Upgraded `@expo/traveling-fastlane-darwin` to 1.9.4 (fixes issues with generating provisioning profiles for ad-hoc builds).
### Removed
- Support for `CLIENT_LOGGER_LEVEL` env variable.
## [0.7.1] - 2019-05-06
### Fixed
- Resolving `fastlane` version when `LC_ALL` is not set.
## [0.7.0] - 2019-05-06
### Added
- Support for custom Android builds.
### Changed
- Refactored logger.
### Fixed
- `platform` log field wrongly set to `ios` for Android builds (when using `turtle-cli`).
- Brought back `jobID` and `experienceName` fields to Android logs.
## [0.6.1] - 2019-04-23
### Added
- Checking fastlane version before running build (turtle-cli).
### Changed
- Updated iOS shell app for SDK 32 to fix AppAuth issue.
- Babel replaced by ttypescript.
- Updated xdl to 53.5.3 and @expo/spawn-async to 1.4.2, so that error stack traces should be more informative now.
### Fixed
- Brought back `jobID` and `experienceName` fields to GCloud logs.
### Removed
- `buidAndroidTarballLocally.sh` script (it has been moved to expo repository - [expo/expo#4022](https://github.com/expo/expo/pull/4022)).
- Updated iOS shell app for SDK 32 to fix AppAuth issue
## [0.6.0] - 2019-04-09
### Changed
- Removed Branch module from Android shell apps for all SDK versions.
- Removed Branch module from Android shell apps for all SDK versions
## [0.5.15] - 2019-04-03
### Added
@@ -132,7 +46,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [0.5.10] - 2019-02-19
### Added
- Registering Turtle version in Redis.
- Register Turtle version in Redis.
### Changed
- Update Android shell app for SDK 32 to fix [update](https://github.com/expo/expo/issues/3504) and [back button](https://github.com/expo/expo/issues/1786) issues.
+3
View File
@@ -16,6 +16,9 @@ This repository contains the codebase for the `turtle-cli` NPM package. The same
### Branches
The `master` branch of the repository should be kept releasable at any time. This way we can continuously release fixes and improvements without costly managing of different branches and issues will be noticed and fixed quickly. This also ensures other contributors can check out the latest version from GitHub and work on it with minimal disruption from other features in progress.
## Setting up the repository for development
*TODO*
## Submitting a pull request
To submit a pull request:
1. Fork the [repository](https://github.com/expo/turtle) and create a feature branch. (Existing contributors can create feature branches without forking. Prefix the branch name with `@your-github-username/`.)
-35
View File
@@ -1,35 +0,0 @@
# Turtle Development
## Prerequisites
- Working `www` service, see [this README](https://github.com/expo/universe/tree/master/server/www/README.md) for more details.
- Own AWS SQS queues. See [this README](https://github.com/expo/universe/tree/master/terraform/turtle-queues/README.md) to learn more.
## Setup
- Run `update-local-secrets` to populate the `.secrets*` files. You'll need to run this any time any secret changes (it shouldn't be very often).
- Run `EXPO_UNIVERSE_DIR=/path/to/universe/dir yarn secrets:init-private YOUR_SQS_NICKNAME`. Alternatively, you can create `.secrets.local-queue` file with the SQS queues' urls yourself (`AWS_SQS_OUT_QUEUE_URL`, `AWS_SQS_ANDROID_QUEUE_URL`, `AWS_SQS_ANDROID_PRIORITY_QUEUE_URL`, `AWS_SQS_IOS_QUEUE_URL`, `AWS_SQS_IOS_PRIORITY_QUEUE_URL`)
- Run `yarn set-workingdir:local` or `yarn set-workingdir:remote`:
- `:local` creates symlinks to the shell apps in `expo` repository (_unfortunately this is currently broken_)
- `:remote` downloads precompiled shell apps
- Run `yarn` to install node modules.
- Run `yarn start:ios` or `yarn start:android` to run Turtle.
## Updating shell apps
_TODO_
## Generating recording files for smoke tests
Smoke tests are powered by [Nock](https://www.npmjs.com/package/nock) which uses JSON files containing responses to the HTTP requests fired by the build process.
To record new responses remove old `recording.json` and run tests and then manually schedule a build with `expo-cli` tool.
As a precaution, the file with HTTP requests/responses contains, among other things, iOS certificates used for the test build, is
encrypted with `git-crypt`. Remember not to revoke them, because then the tests will fail.
## Other notes
Turtle uses Redis to get notifications (from www) about builds cancellations. To make this feature working, you have to use the same Redis instance as www. Therefore, you need to remember about starting www before starting Turtle. If you forget about it, don't worry, Turtle will remind you to run `yarn start-docker` in `server/www` directory.
Turtle uploads build artifacts to S3. This could take some time if you have a poor internet connection. To facilitate local development, there is a feature which allows Turtle to fake artifacts upload. `secrets:init-private` script, which you should have run while setting up Turtle, sets fake upload directory to your user's default `Downloads` directory (we're assuming you're using Mac). If you need to test uploading artifacts to S3, you have to edit `.secrets.local` file and comment or remove `TURTLE_FAKE_UPLOAD` and `TURTLE_FAKE_UPLOAD_DIR` environment variables.
+25 -21
View File
@@ -20,11 +20,9 @@ RUN dpkg --add-architecture i386 && \
libc6:i386\
libncurses5:i386\
libstdc++6:i386\
rsync\
unzip\
yarn\
zlib1g:i386\
supervisor\
--no-install-recommends && \
apt-get clean
@@ -52,8 +50,20 @@ ENV PATH ${ANDROID_HOME}/tools:${PATH}
ENV PATH ${ANDROID_HOME}/tools/bin:${PATH}
ENV PATH ${ANDROID_HOME}/build-tools/28.0.3/:${PATH}
ADD scripts/android/configureAndroidSdk.sh /tmp/configureAndroidSdk.sh
RUN /tmp/configureAndroidSdk.sh --install-all-platforms
RUN yes | sdkmanager --licenses > /dev/null
RUN yes | sdkmanager \
"platforms;android-23" \
"platforms;android-24" \
"platforms;android-25" \
"platforms;android-26" \
"platforms;android-27" \
"platforms;android-28"
RUN yes | sdkmanager "platform-tools"
RUN yes | sdkmanager "build-tools;28.0.3"
RUN yes | sdkmanager \
"extras;android;m2repository" \
"extras;google;m2repository" \
"extras;google;google_play_services"
# Install Android NDK
ENV ANDROID_NDK_VERSION android-ndk-r10e
@@ -89,29 +99,23 @@ RUN rm gradle-4.4-all.zip
ENV GRADLE_HOME /usr/local/gradle-4.4
ENV PATH ${GRADLE_HOME}/bin:$PATH
ADD . /app/turtle
# Install Gulp
RUN npm install -g gulp-cli
RUN mv /app/turtle/workingdir /app && \
for SDK_VERSION in `ls /app/workingdir/android/`; do \
echo "preparing $SDK_VERSION shell app" && \
ADD . /app
RUN for SDK_VERSION in `ls /app/workingdir/android/`; do \
cd /app/workingdir/android/$SDK_VERSION && \
if [ -f universe-package.json ]; then \
mv package.json exponent-package.json && \
mv universe-package.json package.json && \
yarn install && \
mv package.json universe-package.json && \
mv exponent-package.json package.json; \
else \
yarn install; \
fi \
; done && \
mv /app/workingdir /app/turtle
mv exponent-package.json package.json \
; done
WORKDIR /app
ENV NODE_ENV "production"
ENV TURTLE_WORKING_DIR_PATH /app/turtle/workingdir/
ENV TURTLE_WORKING_DIR_PATH /app/workingdir/
WORKDIR /app/turtle
ADD supervisord.conf /etc/supervisor/conf.d/supervisord.conf
CMD ["/usr/bin/supervisord"]
CMD ["node", "./build/server.js"]
+7 -11
View File
@@ -1,6 +1,4 @@
<h3 align=center>
<img src="logo/horizontal.png" width=50%>
</h3>
# Turtle CLI
[![CircleCI](https://img.shields.io/circleci/project/github/expo/turtle/master.svg)](https://circleci.com/gh/expo/turtle)
[![NPM](https://img.shields.io/npm/v/turtle-cli/latest.svg)](https://npmjs.com/package/turtle-cli)
@@ -9,26 +7,25 @@
[![Expo Developers Slack](https://img.shields.io/badge/Expo%20Developers-e01563.svg?logo=slack)](http://slack.expo.io)
[![Expo Forums](https://img.shields.io/badge/Expo%20Forums-blue.svg)](https://forums.expo.io/)
<hr />
Turtle CLI is a command line interface for building Expo standalone apps.
You can use it both on your CI and your private computer.
- [Changelog](CHANGELOG.md)
- [Contributing](CONTRIBUTING.md)
- [Development](DEVELOPMENT.md)
- [Contributing to Turtle CLI](CONTRIBUTING.md)
- [Code of Conduct](CODE_OF_CONDUCT.md)
## Installation
## Glossary
- **Turtle** - script which takes a shell app, fills it with user's code and produces a standalone app
- **Shell app** — precompiled (in case of iOS builds) Expo app which doesn't have user's code
- **Standalone app** - .ipa/.apk file, a user uploads standalone apps to the Google Play Store/Apple App Store
## Installation
[Installation instructions and documentation here.](https://docs.expo.io/versions/latest/distribution/turtle-cli)
## Usage example
See the [expo/turtle-cli-example](https://github.com/expo/turtle-cli-example) repository to learn how to use Turtle CLI in order to build Expo standalone apps on your CI.
## Do you need help?
- Make sure you've read [the Expo Docs](https://docs.expo.io/), especially [Building Standalone Apps on Your CI
](https://docs.expo.io/versions/latest/distribution/turtle-cli) and [Building Standalone Apps](https://docs.expo.io/versions/latest/distribution/building-standalone-apps) guides.
- Search for existing GitHub issues [in this repository](https://github.com/expo/turtle/issues) and in [expo/expo](https://github.com/expo/expo/issues) repository.
@@ -38,6 +35,5 @@ See the [expo/turtle-cli-example](https://github.com/expo/turtle-cli-example) re
* ask a question on [Expo Developers Slack](http://slack.expo.io).
## License
The Turtle CLI source code is made available under the [MIT license](LICENSE).
Some of the dependencies are licensed differently, with the BSD license, for example.
-9
View File
@@ -1,15 +1,6 @@
#!/usr/bin/env node
'use strict';
const subcommand = process.argv[2];
if (subcommand) {
const [, platform] = subcommand.split(':');
if (platform === 'ios' || platform === 'android') {
// so that logs have correct platform set
process.env.PLATFORM = platform;
}
}
require('source-map-support').install();
require('../build/bin/env').initOfflineEnv();
require('../build/bin/index').run('turtle');
+91
View File
@@ -0,0 +1,91 @@
'use strict';
const childProcess = require('child_process');
const util = require('util');
const path = require('path');
const gulp = require('gulp');
const babel = require('gulp-babel');
const changed = require('gulp-changed');
const nodemon = require('gulp-nodemon');
const plumber = require('gulp-plumber');
const sourcemaps = require('gulp-sourcemaps');
const ts = require('gulp-typescript');
const rimraf = require('rimraf');
const rimrafAsync = util.promisify(rimraf);
const tsProject = ts.createProject('tsconfig.json');
gulp.task('clean', async () => {
console.log(`Removing build directories...`);
await rimrafAsync('build');
});
gulp.task('build:server', () => {
console.log(`Compiling the Node.js server JavaScript...`);
return tsProject
.src()
.pipe(plumber())
.pipe(changed('build'))
.pipe(sourcemaps.init())
.pipe(tsProject())
.pipe(babel())
.pipe(
sourcemaps.write('.', {
includeContent: false,
sourceRoot: '../src',
})
)
.pipe(gulp.dest('build'));
});
gulp.task('watch:source', async () => {
console.log(`Watching for changes to the Node.js server JavaScript...`);
gulp.watch('src/**/*.(t|j)s', gulp.series('build:server'));
});
gulp.task('watch:local', createWatchTask('development'));
gulp.task('watch:server', createWatchTask('production'));
function createWatchTask(nodeEnv) {
return async () => {
console.log(`Starting the Node.js server and watching for changes (NODE_ENV=${nodeEnv})...`);
// Run nodemon in a separate process to isolate its side effects on stdio streams
let nodeCmd = 'node';
if (nodeEnv === 'development') {
nodeCmd += ' --inspect=:9315';
}
nodemon({
script: './build/server.js',
execMap: {
js: nodeCmd,
},
env: {
NODE_ENV: nodeEnv,
},
watch: ['./build'],
stdout: false,
readable: false,
}).on('readable', function() {
const bunyan = childProcess.fork(
path.join('.', 'node_modules', '.bin', 'bunyan'),
['-o', 'short', '-l', 'debug', '--color'],
{ silent: true }
);
bunyan.stdout.pipe(process.stdout);
bunyan.stderr.pipe(process.stderr);
this.stdout.pipe(bunyan.stdin);
this.stderr.pipe(bunyan.stdin);
});
};
}
gulp.task(
'watch',
gulp.series('clean', 'build:server', gulp.parallel('watch:source', 'watch:local'))
);
gulp.task('default', gulp.series('watch'));
+1 -11
View File
@@ -22,7 +22,7 @@ spec:
environment: ${NAMESPACE}
spec:
restartPolicy: Always
terminationGracePeriodSeconds: 1200 ## wait 20 minutes for any builds to complete
terminationGracePeriodSeconds: 1800 ## wait 30 minutes for any builds to complete
tolerations:
- key: dedicated
operator: Equal
@@ -128,16 +128,6 @@ spec:
secretKeyRef:
name: turtle-js-config
key: api_port
- name: DATADOG_API_KEY
valueFrom:
secretKeyRef:
name: turtle-js-config
key: datadog_api_key
- name: DATADOG_APP_KEY
valueFrom:
secretKeyRef:
name: turtle-js-config
key: datadog_app_key
# other vars
- name: GRADLE_DAEMON_DISABLED
value: "1"
Binary file not shown.

Before

Width:  |  Height:  |  Size: 69 KiB

-228
View File
@@ -1,228 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="151.68256mm"
height="150.14914mm"
viewBox="0 0 151.68255 150.14914"
version="1.1"
id="svg8"
sodipodi:docname="logo-only.svg"
inkscape:version="0.92.4 (5da689c313, 2019-01-14)">
<defs
id="defs2">
<clipPath
clipPathUnits="userSpaceOnUse"
id="clipPath16">
<path
d="M 0,192 H 192 V 0 H 0 Z"
id="path18"
inkscape:connector-curvature="0" />
</clipPath>
<clipPath
clipPathUnits="userSpaceOnUse"
id="clipPath26">
<path
d="M -4.88281e-4,192 H 192 V -4.88281e-4 H -4.88281e-4 Z"
id="path28"
inkscape:connector-curvature="0" />
</clipPath>
<clipPath
clipPathUnits="userSpaceOnUse"
id="clipPath34">
<path
d="M -4.88281e-4,192 H 192 V -4.88281e-4 H -4.88281e-4 Z"
id="path36"
inkscape:connector-curvature="0" />
</clipPath>
<clipPath
clipPathUnits="userSpaceOnUse"
id="clipPath38">
<path
d="M 0,192 H 192 V 0 H 0 Z"
id="path40"
inkscape:connector-curvature="0" />
</clipPath>
<clipPath
clipPathUnits="userSpaceOnUse"
id="clipPath42">
<path
d="M 0,192 H 192 V 0 H 0 Z"
id="path44"
inkscape:connector-curvature="0" />
</clipPath>
<clipPath
clipPathUnits="userSpaceOnUse"
id="clipPath116">
<path
d="M 0,192 H 192 V -4.88281e-4 H 0 Z"
id="path118"
inkscape:connector-curvature="0" />
</clipPath>
<clipPath
clipPathUnits="userSpaceOnUse"
id="clipPath120">
<path
d="M 0,192 H 192 V 4.88281e-4 H 0 Z"
id="path122"
inkscape:connector-curvature="0" />
</clipPath>
<clipPath
clipPathUnits="userSpaceOnUse"
id="clipPath124">
<path
d="M 192,192 H 0 V 0 h 192 z"
id="path126"
inkscape:connector-curvature="0" />
</clipPath>
</defs>
<sodipodi:namedview
id="base"
pagecolor="#3d3d3d"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:zoom="0.49497474"
inkscape:cx="-192.6803"
inkscape:cy="237.87441"
inkscape:document-units="px"
inkscape:current-layer="layer2"
showgrid="false"
units="mm"
inkscape:showpageshadow="false"
inkscape:window-width="1366"
inkscape:window-height="705"
inkscape:window-x="-8"
inkscape:window-y="-8"
inkscape:window-maximized="1"
inkscape:object-paths="true"
fit-margin-top="0"
fit-margin-left="0"
fit-margin-right="0"
fit-margin-bottom="0" />
<metadata
id="metadata5">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:groupmode="layer"
id="layer2"
inkscape:label="Layer 2"
transform="translate(-29.1587,91.282943)">
<g
id="g6678"
transform="translate(792.65276,-237.50395)">
<path
inkscape:connector-curvature="0"
id="path6638"
d="m -687.31547,152.53653 -16.98554,9.86968 v 19.204 l 15.52774,-8.98033 a 2.9187589,2.9239263 0 0 1 1.52654,-0.38809 2.9187589,2.9239263 0 0 1 1.39009,0.38809 l 15.52721,8.98033 v -19.204 z"
style="opacity:1;fill:#00002f;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1.32291651;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:stroke fill markers" />
<path
inkscape:connector-curvature="0"
id="path6640"
d="m -687.3165,178.53959 -48.20947,27.88306 v 55.7661 l 48.20947,27.88306 48.20996,-27.88306 v -55.7661 z"
style="opacity:1;fill:#00002f;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1.32291651;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:stroke fill markers" />
<path
inkscape:connector-curvature="0"
id="path6642"
d="m -738.44361,184.99346 -16.985,9.86968 v 19.74092 l 14.06631,8.17418 v -18.04386 a 2.9187589,2.9239263 0 0 1 1.45986,-2.5337 l 15.57269,-9.00617 z"
style="opacity:1;fill:#00002f;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1.32291651;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:stroke fill markers" />
<path
inkscape:connector-curvature="0"
id="path6644"
d="m -634.61997,184.99346 -14.89522,8.65426 14.78772,8.55296 a 2.9187589,2.9239263 0 0 1 1.45986,2.5337 v 18.95491 l 15.63418,-9.08523 v -19.74092 z"
style="opacity:1;fill:#00002f;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1.32291651;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:stroke fill markers" />
<path
inkscape:connector-curvature="0"
id="path6646"
d="m -741.3623,245.97166 -16.30752,9.47642 v 19.74091 l 16.98398,9.87124 16.47753,-9.57513 -15.69413,-9.07748 a 2.9187589,2.9239263 0 0 1 -1.45986,-2.5306 z"
style="opacity:1;fill:#00002f;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1.32291651;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:stroke fill markers" />
<path
inkscape:connector-curvature="0"
id="path6648"
d="m -633.26761,247.66664 v 16.21038 a 2.9187589,2.9239263 0 0 1 -1.45986,2.5306 l -17.35915,10.0397 v 5.2e-4 l 0.20309,-0.11782 15.02182,8.73022 16.98655,-9.87124 v -19.74091 z"
style="opacity:1;fill:#00002f;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1.32291651;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:stroke fill markers" />
<g
id="g6654"
transform="translate(-172.35714,278.19048)">
<path
inkscape:connector-curvature="0"
id="path6650"
d="m -514.88984,-105.94886 a 2.918759,2.9239263 0 0 0 -1.52625,0.388 l -51.12915,29.57121 a 2.918759,2.9239263 0 0 0 -1.45977,2.5338 v 59.142302 a 2.918759,2.9239263 0 0 0 1.45977,2.530901 l 51.12915,29.571197 a 2.918759,2.9239263 0 0 0 2.91661,0 l 51.12917,-29.571197 a 2.918759,2.9239263 0 0 0 1.45977,-2.530901 V -73.45585 a 2.918759,2.9239263 0 0 0 -1.45977,-2.5338 l -51.12917,-29.57121 a 2.918759,2.9239263 0 0 0 -1.39036,-0.388 z m -0.0694,6.298204 48.20963,27.882906 v 55.766001 l -48.20963,27.883001 -48.20965,-27.883001 V -71.76775 Z"
style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-feature-settings:normal;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;text-orientation:mixed;dominant-baseline:auto;baseline-shift:baseline;text-anchor:start;white-space:normal;shape-padding:0;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;vector-effect:none;fill:#21aba5;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:3.17500019;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:stroke fill markers;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" />
<path
inkscape:connector-curvature="0"
id="path6652"
d="m -514.88885,-85.717946 a 2.9118515,2.9308623 0 0 0 -1.52531,0.3904 l -33.47339,19.451796 a 2.9118515,2.9308623 0 0 0 -1.45528,2.5391 v 38.903402 a 2.9118515,2.9308623 0 0 0 1.45528,2.537198 l 33.47339,19.4517023 a 2.9118515,2.9308623 0 0 0 2.91246,0 l 33.47151,-19.4517023 a 2.9118515,2.9308623 0 0 0 1.45528,-2.537198 V -63.33665 a 2.9118515,2.9308623 0 0 0 -1.45528,-2.5391 l -33.47151,-19.451796 a 2.9118515,2.9308623 0 0 0 -1.38715,-0.3904 z m -0.0699,6.312396 30.56093,17.760299 V -26.12465 L -514.95875,-8.3663467 -545.5197,-26.12665 v -35.518601 z"
style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-feature-settings:normal;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;text-orientation:mixed;dominant-baseline:auto;baseline-shift:baseline;text-anchor:start;white-space:normal;shape-padding:0;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;vector-effect:none;fill:#21aba5;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:3.17500019;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:stroke fill markers;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" />
</g>
<g
id="g6660"
transform="translate(-172.35714,278.19048)">
<path
id="path6656"
d="m -519.66912,-32.288849 -5.84307,5.844 h 25.07044 a 2.9220001,2.9220001 0 0 0 0,-5.844 z"
style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-feature-settings:normal;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;text-orientation:mixed;dominant-baseline:auto;baseline-shift:baseline;text-anchor:start;white-space:normal;shape-padding:0;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;vector-effect:none;fill:#45eba5;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:3.17499971;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
inkscape:connector-curvature="0" />
<path
inkscape:connector-curvature="0"
id="path6658"
d="m -529.50808,-61.355249 a 2.9213416,2.9213416 0 0 0 -2.0341,5.017 l 12.45256,12.452501 -12.45256,12.452599 a 2.9213416,2.9213416 0 1 0 4.1329,4.129201 l 14.51539,-14.515401 a 2.9213416,2.9213416 0 0 0 0,-4.1328 l -14.51539,-14.515401 a 2.9213416,2.9213416 0 0 0 -2.0988,-0.887699 z"
style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-feature-settings:normal;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;text-orientation:mixed;dominant-baseline:auto;baseline-shift:baseline;text-anchor:start;white-space:normal;shape-padding:0;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;vector-effect:none;fill:#45eba5;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:3.17500019;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" />
</g>
<g
id="g6676"
transform="translate(-172.35714,278.19048)">
<g
id="g6672">
<path
inkscape:connector-curvature="0"
style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-feature-settings:normal;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;text-orientation:mixed;dominant-baseline:auto;baseline-shift:baseline;text-anchor:start;white-space:normal;shape-padding:0;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;vector-effect:none;fill:#21aba5;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:3.17499971;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:stroke fill markers;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
d="m -514.89008,-131.96866 a 2.9118515,2.9308623 0 0 0 -1.52426,0.3917 l -19.89585,11.5625 a 2.9118515,2.9308623 0 0 0 -1.45673,2.5388 v 23.125004 a 2.9118515,2.9308623 0 0 0 0.18924,1.02881 l 5.63388,-3.25861 v -19.203604 l 16.98527,-9.87 16.98619,9.87 v 19.203604 l 5.6348,3.25861 a 2.9118515,2.9308623 0 0 0 0.18734,-1.02881 v -23.125004 a 2.9118515,2.9308623 0 0 0 -1.4558,-2.5388 l -19.89678,-11.5625 a 2.9118515,2.9308623 0 0 0 -1.3873,-0.3917 z"
id="path6662" />
<path
inkscape:connector-curvature="0"
style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-feature-settings:normal;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;text-orientation:mixed;dominant-baseline:auto;baseline-shift:baseline;text-anchor:start;white-space:normal;shape-padding:0;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;vector-effect:none;fill:#21aba5;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:3.17499971;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:stroke fill markers;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
d="m -566.01799,-99.511956 a 2.9118515,2.9308623 0 0 0 -1.52421,0.3918 l -19.89585,11.56251 a 2.9118515,2.9308623 0 0 0 -1.45673,2.5387 v 23.124998 a 2.9118515,2.9308623 0 0 0 1.45673,2.537899 l 18.43343,10.7124 v -6.768201 l -14.06704,-8.174598 v -19.740898 l 16.98522,-9.8699 14.11368,8.2012 5.83733,-3.3766 -18.49521,-10.74751 a 2.9118515,2.9308623 0 0 0 -1.38735,-0.3918 z"
id="path6664" />
<path
inkscape:connector-curvature="0"
style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-feature-settings:normal;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;text-orientation:mixed;dominant-baseline:auto;baseline-shift:baseline;text-anchor:start;white-space:normal;shape-padding:0;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;vector-effect:none;fill:#21aba5;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:3.17499971;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:stroke fill markers;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
d="m -462.19425,-99.511956 a 2.9118515,2.9308623 0 0 0 -1.52421,0.3918 l -19.27591,11.20211 5.83641,3.3755 14.89526,-8.6547 16.98619,9.8699 v 19.740898 l -15.63409,9.085498 v 6.765401 a 2.9118515,2.9308623 0 0 0 0.10365,-0.058 l 19.89775,-11.5625 a 2.9118515,2.9308623 0 0 0 1.45483,-2.537801 v -23.124996 a 2.9118515,2.9308623 0 0 0 -1.45483,-2.5388 l -19.89775,-11.56251 a 2.9118515,2.9308623 0 0 0 -1.3873,-0.3918 z"
id="path6666" />
<path
inkscape:connector-curvature="0"
style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-feature-settings:normal;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;text-orientation:mixed;dominant-baseline:auto;baseline-shift:baseline;text-anchor:start;white-space:normal;shape-padding:0;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;vector-effect:none;fill:#21aba5;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:3.17499971;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:stroke fill markers;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
d="m -569.00462,-38.844049 a 2.9118515,2.9308623 0 0 0 -0.77875,0.308999 l -19.89683,11.562501 a 2.9118515,2.9308623 0 0 0 -1.45672,2.538801 v 23.1250003 a 2.9118515,2.9308623 0 0 0 1.45672,2.537801 l 19.89683,11.5624977 a 2.9118515,2.9308623 0 0 0 2.91058,0 l 19.89775,-11.5624977 a 2.9118515,2.9308623 0 0 0 0.77778,-0.66460201 l -5.65379,-3.26999799 -16.47749,9.575199 -16.98435,-9.8709 V -22.74215 l 16.30827,-9.477299 z"
id="path6668" />
<path
inkscape:connector-curvature="0"
style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-feature-settings:normal;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;text-orientation:mixed;dominant-baseline:auto;baseline-shift:baseline;text-anchor:start;white-space:normal;shape-padding:0;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;vector-effect:none;fill:#21aba5;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:3.17499971;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:stroke fill markers;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
d="m -460.9106,-37.293149 v 6.769201 l 13.39292,7.781798 v 19.7409023 l -16.9862,9.8709 -15.02174,-8.729901 -5.83733,3.375502 19.40327,11.2762977 a 2.9118515,2.9308623 0 0 0 2.91155,0 l 19.89678,-11.5624977 a 2.9118515,2.9308623 0 0 0 1.4558,-2.537801 V -24.433748 a 2.9118515,2.9308623 0 0 0 -1.4558,-2.538801 z"
id="path6670" />
</g>
<path
style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-feature-settings:normal;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;text-orientation:mixed;dominant-baseline:auto;baseline-shift:baseline;text-anchor:start;white-space:normal;shape-padding:0;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;vector-effect:none;fill:#21aba5;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:3.17500019;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
d="m -531.46275,-116.06384 -0.48112,0.27957 v 5.56039 a 2.9213415,2.9213415 0 0 0 0.0171,0 h 33.93644 a 2.9213415,2.9213415 0 0 0 0.0181,-5.2e-4 v -5.55987 l -0.48109,-0.27957 z"
id="path6674"
inkscape:connector-curvature="0" />
</g>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 24 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 80 KiB

-6
View File
@@ -1,6 +0,0 @@
{
"watch": ["src"],
"ext": "ts",
"ignore": ["src/**/*.test.ts"],
"exec": "ts-node -r tsconfig-paths/register --files src/server.ts"
}
+43 -24
View File
@@ -1,22 +1,28 @@
{
"name": "turtle-cli",
"version": "0.8.7",
"version": "0.6.0",
"description": "The command-line tool for building standalone Expo apps",
"preferGlobal": true,
"scripts": {
"start": "EXPENV_OVERRIDES=local-common,local,local-queue expenv nodemon | bunyan -o short -l debug --color",
"start": "EXPENV_OVERRIDES=local-common,local,local-queue expenv gulp",
"start:ios": "PLATFORM=ios yarn start",
"start:android": "PLATFORM=android yarn start",
"watch": "EXPENV_OVERRIDES=local-common,local,local-queue expenv gulp watch:source",
"bin:turtle": "./bin/turtle.js",
"lint": "tslint -c tslint.json 'src/**/*.ts'",
"lint:fix": "yarn lint --fix",
"prepare": "rimraf ./build && yarn build",
"build": "ttsc",
"prepare": "yarn build",
"build": "gulp build:server",
"build:production": "./scripts/build.sh",
"watch": "ttsc --watch",
"build:android-tarball:local": "./scripts/android/buildAndroidTarballLocally.sh",
"secrets:init-private": "./scripts/initPrivateSecrets.sh",
"set-workingdir:remote": "./scripts/setWorkingdirMode.sh remote",
"set-workingdir:local": "./scripts/setWorkingdirMode.sh local",
"docker:pull": "retry5 gcloud docker -- pull gcr.io/exponentjs/turtle-android:latest",
"docker:pull:tag": "retry5 gcloud docker -- pull gcr.io/exponentjs/turtle-android:${TAG}",
"docker:build": "yarn build:production android && gcloud docker -- build -t gcr.io/exponentjs/turtle-android:latest -t gcr.io/exponentjs/turtle-android:${TAG} .",
"docker:push": "retry5 gcloud docker -- push gcr.io/exponentjs/turtle-android:${TAG} && retry5 gcloud docker -- push gcr.io/exponentjs/turtle-android:latest",
"deploy:android": "./scripts/android/deploy.sh",
"test": "yarn jest",
"test:integration": "yarn test --config jest/integration-config.json",
"test:unit": "yarn test --config jest/unit-config.json",
@@ -24,7 +30,14 @@
"test:smoke:android": "PLATFORM=android yarn test:smoke --testNamePattern=android",
"test:smoke:ios": "PLATFORM=ios yarn test:smoke --testNamePattern=iOS",
"release": "./scripts/release.sh",
"release:beta": "./scripts/releaseBeta.sh"
"release:beta": "./scripts/releaseBeta.sh",
"ssh:android:pods-names": "./scripts/ssh/findAndroidPods.sh",
"ssh:android:connect": "./scripts/ssh/connectToAndroidPod.sh",
"ssh:ios:ips": "./scripts/ssh/findIosIps.sh",
"set-priorities": "yarn set-priorities:local",
"set-priorities:local": "EXPENV_OVERRIDES=local-common,local,local-queue expenv babel-node scripts/setConfigurations.js",
"set-priorities:staging": "expenv babel-node scripts/setConfigurations.js",
"set-priorities:production": "EXPENV_OVERRIDES=prod expenv babel-node scripts/setConfigurations.js"
},
"husky": {
"hooks": {
@@ -43,7 +56,7 @@
"files": [
"bin",
"build",
"scripts/android/configureAndroidSdk.sh",
"scripts/cli",
"shellTarballs"
],
"keywords": [
@@ -67,35 +80,42 @@
"@types/fs-extra": "^5.0.4",
"@types/jest": "^23.3.9",
"@types/joi": "^13.4.3",
"@types/lodash": "^4.14.129",
"@types/lodash": "^4.14.109",
"@types/progress": "^2.0.1",
"@types/raven": "^2.5.1",
"@types/request": "^2.47.1",
"@types/semver": "^6.0.0",
"@types/sharp": "^0.17.8",
"@types/tar": "^4.0.0",
"@types/uuid": "^3.4.3",
"@types/which": "^1.3.1",
"babel-cli": "^6.26.0",
"babel-core": "^6.26.3",
"babel-jest": "^23.6.0",
"babel-plugin-module-resolver": "^3.1.1",
"babel-preset-env": "^1.6.1",
"gulp": "github:gulpjs/gulp#4.0",
"gulp-babel": "^7.0.1",
"gulp-changed": "^3.2.0",
"gulp-nodemon": "^2.4.1",
"gulp-plumber": "^1.2.0",
"gulp-sourcemaps": "^2.6.4",
"gulp-typescript": "^4.0.2",
"husky": "^1.3.1",
"jest": "^23.6.0",
"lint-staged": "^8.1.5",
"nock": "^10.0.1",
"nodemon": "^1.18.11",
"nodemon": "^1.18.7",
"release-it": "^10.0.0",
"rimraf": "^2.6.2",
"source-map-loader": "^0.2.3",
"ts-jest": "^23.10.4",
"ts-node": "^8.0.3",
"ts-transformer-imports": "^0.4.1",
"tsconfig-paths": "^3.8.0",
"tslint": "^5.10.0",
"tslint-eslint-rules": "^5.4.0",
"ttypescript": "^1.5.6",
"typescript": "^3.4.2"
"typescript": "^3.0.1"
},
"dependencies": {
"@expo/spawn-async": "^1.4.2",
"@expo/xdl": "55.0.7",
"@google-cloud/logging-bunyan": "^0.9.5",
"@expo/spawn-async": "^1.4.0",
"@google-cloud/logging-bunyan": "^0.10.1",
"async-retry": "^1.2.1",
"aws-sdk": "^2.226.1",
"bunyan": "^1.8.12",
@@ -104,24 +124,23 @@
"commander": "^2.15.1",
"copy": "^0.3.1",
"decompress": "^4.2.0",
"dogapi": "^2.8.3",
"fs-extra": "^6.0.0",
"ioredis": "^3.2.2",
"is-semver": "^1.0.8",
"joi": "^13.2.0",
"lodash": "^4.17.11",
"lodash": "^4.17.10",
"progress": "^2.0.0",
"raven": "^2.6.2",
"request": "^2.88.0",
"request-progress": "^3.0.0",
"semver": "^6.0.0",
"sharp": "0.22.1",
"sharp": "^0.21.0",
"source-map-support": "^0.5.6",
"tar": "^4.4.6",
"uuid": "^3.2.1",
"which": "^1.3.1"
"which": "^1.3.1",
"xdl": "53.1.6-alpha.2"
},
"optionalDependencies": {
"@expo/traveling-fastlane-darwin": "1.9.9"
"@expo/traveling-fastlane-darwin": "^1.8.1"
}
}
+32
View File
@@ -0,0 +1,32 @@
#!/usr/bin/env bash
set -xeo pipefail
ROOT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )"/../.. && pwd )"
ARTIFACTS_DIR="$ROOT_DIR/artifacts"
TEMP_DIR="/tmp/android-shell-app"
mkdir -p $ARTIFACTS_DIR
rm -rf $TEMP_DIR
mkdir -p $TEMP_DIR
ln -s ${EXPO_UNIVERSE_DIR}/exponent/tools-public $TEMP_DIR/tools-public
ln -s ${EXPO_UNIVERSE_DIR}/exponent/secrets $TEMP_DIR/secrets
ln -s ${EXPO_UNIVERSE_DIR}/exponent/template-files $TEMP_DIR/template-files
ln -s ${EXPO_UNIVERSE_DIR}/exponent/android $TEMP_DIR/android
ln -s ${EXPO_UNIVERSE_DIR}/exponent/package.json $TEMP_DIR/package.json
ln -s ${EXPO_UNIVERSE_DIR}/exponent/expokit-npm-package $TEMP_DIR/expokit-npm-package
ln -s ${EXPO_UNIVERSE_DIR}/exponent/packages $TEMP_DIR/packages
ln -s ${EXPO_UNIVERSE_DIR}/libraries/eslint-config-universe $TEMP_DIR/eslint-config-universe
ln -s ${EXPO_UNIVERSE_DIR}/libraries/json-file $TEMP_DIR/json-file
ln -s ${EXPO_UNIVERSE_DIR}/libraries/osascript $TEMP_DIR/osascript
ln -s ${EXPO_UNIVERSE_DIR}/libraries/schemer $TEMP_DIR/schemer
ln -s ${EXPO_UNIVERSE_DIR}/package.json $TEMP_DIR/universe-package.json
# generate dynamic macros
mkdir -p $TEMP_DIR/android/expoview/src/main/java/host/exp/exponent/generated/
cd $TEMP_DIR/tools-public
./generate-dynamic-macros-android.sh
rm -rf $TEMP_DIR/secrets
cd $TEMP_DIR; tar -czhf $ARTIFACTS_DIR/android-shell-builder.tar.gz .
rm -rf $TEMP_DIR
-20
View File
@@ -1,20 +0,0 @@
#!/usr/bin/env bash
SDKMANAGER=$ANDROID_HOME/tools/bin/sdkmanager
yes | $SDKMANAGER --licenses > /dev/null
if [[ "$1" = "--install-all-platforms" ]]; then
yes | $SDKMANAGER \
"platforms;android-23" \
"platforms;android-24" \
"platforms;android-25" \
"platforms;android-26" \
"platforms;android-27" | grep -v '='; [[ ${PIPESTATUS[1]} = 0 ]]
fi
yes | $SDKMANAGER "platforms;android-28" | grep -v '='; [[ ${PIPESTATUS[1]} = 0 ]]
yes | $SDKMANAGER "platform-tools" | grep -v '='; [[ ${PIPESTATUS[1]} = 0 ]]
yes | $SDKMANAGER "build-tools;28.0.3" | grep -v '='; [[ ${PIPESTATUS[1]} = 0 ]]
yes | $SDKMANAGER \
"extras;android;m2repository" \
"extras;google;m2repository" \
"extras;google;google_play_services" | grep -v '='; [[ ${PIPESTATUS[1]} = 0 ]]
@@ -1,8 +0,0 @@
#!/usr/bin/env bash
printf "READY\n"; # signal to superivsord that listener is ready
while read line; do
echo "[supervisord:eventlistener] Processing Event: $line" >&2;
kill -SIGQUIT $PPID
done < /dev/stdin
+12
View File
@@ -0,0 +1,12 @@
#!/bin/bash
SDKMANAGER=$ANDROID_HOME/tools/bin/sdkmanager
yes | $SDKMANAGER --licenses > /dev/null
yes | $SDKMANAGER "platforms;android-28"
yes | $SDKMANAGER "platform-tools"
yes | $SDKMANAGER "build-tools;28.0.3"
yes | $SDKMANAGER \
"extras;android;m2repository" \
"extras;google;m2repository" \
"extras;google;google_play_services"
-76
View File
@@ -1,76 +0,0 @@
#!/usr/bin/env bash
set -eo pipefail
SCRIPT_NAME=`basename "$0"`
DEPLOY_ENDPOINT_URL="https://circleci.com/api/v1.1/project/github/expo/turtle-deploy/tree/master"
if [[ -z "$1" || -z "$2" ]]; then
echo "usage: ${bold}$SCRIPT_NAME PLATFORM ENVIRONMENT [FORCE]${normal}"
exit 1
fi
PLATFORM="$1"
ENVIRONMENT="$2"
if [[ "$3" = "--force" ]]; then
FORCE_DEPLOY="true"
fi
if [[ "$PLATFORM" != "android" && "$PLATFORM" != "ios" ]]; then
echo "unsupported PLATFORM passed: ${bold}$PLATFORM${normal}"
echo "valid values are: ${bold}android${normal}, ${bold}ios${normal}"
exit 1
fi
if [[ "$ENVIRONMENT" != "staging" && "$ENVIRONMENT" != "production" ]]; then
echo "unsupported ENVIRONMENT passed: ${bold}$ENVIRONMENT${normal}"
echo "valid values are: ${bold}staging${normal}, ${bold}production${normal}"
exit 1
fi
if [[ ! -z "$CIRCLE_SHA1" ]]; then
TURTLE_COMMIT="$CIRCLE_SHA1"
elif [[ -z "$FORCE_DEPLOY" ]]; then
read -p "You're about to schedule a deploy, are you sure you want to deploy the current commit? (y/n) " -n 1 -r
echo # (optional) move to a new line
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
echo "exiting..."
exit 0
else
TURTLE_COMMIT=`git log --pretty=format:%H -1`
fi
fi
if [[ -z "$TURTLE_BOT_CIRCLE_API_USER_TOKEN" ]]; then
echo "Please set the TURTLE_BOT_CIRCLE_API_USER_TOKEN env variable."
exit 1
fi
if [[ $FORCE_DEPLOY = "true" ]]; then
curl -X POST \
--header "Content-Type: application/json" \
-u $TURTLE_BOT_CIRCLE_API_USER_TOKEN: \
-d '
{
"build_parameters": {
"CIRCLE_JOB": "deploy_'$PLATFORM'",
"TURTLE_ENV": "'$ENVIRONMENT'",
"TURTLE_FORCE_DEPLOY": "true"
}
}' \
$DEPLOY_ENDPOINT_URL | jq -r .build_url
else
curl -X POST \
--header "Content-Type: application/json" \
-u $TURTLE_BOT_CIRCLE_API_USER_TOKEN: \
-d '
{
"build_parameters": {
"CIRCLE_JOB": "deploy_'$PLATFORM'",
"TURTLE_ENV": "'$ENVIRONMENT'",
"TURTLE_COMMIT": "'$TURTLE_COMMIT'"
}
}' \
$DEPLOY_ENDPOINT_URL | jq -r .build_url
fi
+4 -8
View File
@@ -13,17 +13,13 @@ fi
TURTLE_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )"/.. && pwd )"
TERRAFORM_DIR="${EXPO_UNIVERSE_DIR}/terraform"
ANDROID_NORMAL=`cd $TERRAFORM_DIR; terraform state show module.turtle_queues_local_$1.aws_sqs_queue.jobs_android_in | head -1 | cut -d"=" -f2 | xargs`
ANDROID_PRIORITY=`cd $TERRAFORM_DIR; terraform state show module.turtle_queues_local_$1.aws_sqs_queue.jobs_android_in_priority | head -1 | cut -d"=" -f2 | xargs`
IOS_NORMAL=`cd $TERRAFORM_DIR; terraform state show module.turtle_queues_local_$1.aws_sqs_queue.jobs_ios_in | head -1 | cut -d"=" -f2 | xargs`
IOS_PRIORITY=`cd $TERRAFORM_DIR; terraform state show module.turtle_queues_local_$1.aws_sqs_queue.jobs_ios_in_priority | head -1 | cut -d"=" -f2 | xargs`
ANDROID=`cd $TERRAFORM_DIR; terraform state show module.turtle_queues_local_$1.aws_sqs_queue.jobs_android_in | head -1 | cut -d"=" -f2 | xargs`
IOS=`cd $TERRAFORM_DIR; terraform state show module.turtle_queues_local_$1.aws_sqs_queue.jobs_ios_in | head -1 | cut -d"=" -f2 | xargs`
OUT=`cd $TERRAFORM_DIR; terraform state show module.turtle_queues_local_$1.aws_sqs_queue.jobs_out | head -1 | cut -d"=" -f2 | xargs`
cat << EOF > $TURTLE_DIR/.secrets.local-queue
AWS_SQS_ANDROID_QUEUE_URL="$ANDROID_NORMAL"
AWS_SQS_ANDROID_PRIORITY_QUEUE_URL="$ANDROID_PRIORITY"
AWS_SQS_IOS_QUEUE_URL="$IOS_NORMAL"
AWS_SQS_IOS_PRIORITY_QUEUE_URL="$IOS_PRIORITY"
AWS_SQS_ANDROID_QUEUE_URL="$ANDROID"
AWS_SQS_IOS_QUEUE_URL="$IOS"
AWS_SQS_OUT_QUEUE_URL="$OUT"
EOF
+73
View File
@@ -0,0 +1,73 @@
import { env } from 'turtle/utils/env';
import { connect, RedisClient } from 'turtle/utils/redis';
import { PLATFORMS } from 'turtle/constants';
import {
createConfigurationsKey,
createDefaultConfigurationKey,
HIGH_CONFIGURATION,
NORMAL_CONFIGURATION,
} from 'turtle/utils/priorities';
const TIMEOUT = 3000;
// Turtle before doing anything (and after every build) checks for the first config
// (an element in config table with key=i) that is not taken (config.i is null/outdated)
// or is taken by this process itself.
// If Turtle dies unexpectedly the configuration will become free after TTL expires,
// so the state (number of HP/NP turtles) should come back to normal soon.
// For example, if a configuration table looks like this:
// [ HIGH_CONFIGURATION, NORMAL_CONFIGURATION, HIGH_CONFIGURATION ]
// If there is one turtle it runs with High Priority first.
// If there are two turtles, one runs with High Priority first, the second one with Normal Priority first.
// If the first dies, and the third is started in his place, it gets 3rd config (again HP-first),
// but after some time, because of TTL for config reservations,
// they should come back to using configurations no. 1 and 2.
// If there are no more available configurations from the table, the default configuration is used.
const AVAILABLE_CONFIGURATIONS_IOS = [
HIGH_CONFIGURATION,
HIGH_CONFIGURATION,
NORMAL_CONFIGURATION,
NORMAL_CONFIGURATION,
HIGH_CONFIGURATION,
NORMAL_CONFIGURATION,
];
const AVAILABLE_CONFIGURATIONS_ANDROID = [
HIGH_CONFIGURATION,
HIGH_CONFIGURATION,
NORMAL_CONFIGURATION,
NORMAL_CONFIGURATION,
HIGH_CONFIGURATION,
HIGH_CONFIGURATION,
NORMAL_CONFIGURATION,
NORMAL_CONFIGURATION,
HIGH_CONFIGURATION,
HIGH_CONFIGURATION,
NORMAL_CONFIGURATION,
NORMAL_CONFIGURATION,
HIGH_CONFIGURATION,
HIGH_CONFIGURATION,
NORMAL_CONFIGURATION,
NORMAL_CONFIGURATION,
];
const DEFAULT_CONFIGURATION = HIGH_CONFIGURATION;
async function run() {
const redis = await connect(TIMEOUT, RedisClient.Configuration);
await redis.set(createConfigurationsKey(PLATFORMS.IOS), JSON.stringify(AVAILABLE_CONFIGURATIONS_IOS));
await redis.set(createConfigurationsKey(PLATFORMS.ANDROID), JSON.stringify(AVAILABLE_CONFIGURATIONS_ANDROID));
await redis.set(createDefaultConfigurationKey(PLATFORMS.IOS), JSON.stringify(DEFAULT_CONFIGURATION));
await redis.set(createDefaultConfigurationKey(PLATFORMS.ANDROID), JSON.stringify(DEFAULT_CONFIGURATION));
redis.disconnect();
}
run()
.then(() => console.log('All done'))
.catch(err => console.error('Error:', err));
+35 -51
View File
@@ -6,44 +6,42 @@ DOWNLOAD_DIR="${EXPO_DOWNLOAD_DIR:-$HOME/Downloads}"
mkdir -p $DOWNLOAD_DIR
if [ ! -f "$ROOT_DIR/.secrets.local-queue" ]; then
echo "You need to run \"yarn secrets:init-private [SQS_NAME]\" or create the .secrets.local-queue file yourself with AWS_SQS_ANDROID_QUEUE_URL, AWS_SQS_ANDROID_PRIORITY_QUEUE_URL, AWS_SQS_IOS_QUEUE_URL, AWS_SQS_IOS_PRIORITY_QUEUE_URL and AWS_SQS_OUT_QUEUE_URL variables."
if [ ! -f "$ROOT_DIR/.secrets.local-queue" ]; then
echo "You need to run \"yarn secrets:init-private [SQS_NAME]\" or create .secrets.local-queue file yourself with AWS_SQS_ANDROID_QUEUE_URL, AWS_SQS_IOS_QUEUE_URL and AWS_SQS_OUT_QUEUE_URL variables."
exit 1
fi
# @dsokal: this is currently broken
#
# setup_local() {
# ensure_link_exists() {
# if [ ! -L "$2" ]; then
# ln -s $1 $2
# echo "$2 link created"
# fi
# }
# WORKINGDIR="$ROOT_DIR/workingdir/local"
#
# if [[ -d $WORKINGDIR ]]; then
# echo "Local workingdir already prepared"
# return
# fi
#
# if [[ -z "$EXPO_UNIVERSE_DIR" ]]; then
# echo "EXPO_UNIVERSE_DIR needs to be set"
# exit -1
# fi
#
# cd $EXPO_UNIVERSE_DIR/exponent && yarn
# cd $EXPO_UNIVERSE_DIR/exponent/tools-public && yarn && ./generate-dynamic-macros-android.sh
#
# mkdir -p $WORKINGDIR
# echo "$WORKINGDIR directory created"
#
# LINKS=("template-files" "package.json" "android" "expokit-npm-package" "packages" "client-builds" "cpp" "exponent-view-template" "ios" "shellAppBase-builds" "shellAppWorkspaces" "node_modules")
#
# for LINKNAME in ${LINKS[@]}; do
# ensure_link_exists "$EXPO_UNIVERSE_DIR/exponent/$LINKNAME" "$WORKINGDIR/$LINKNAME"
# done
# }
setup_local() {
ensure_link_exists() {
if [ ! -L "$2" ]; then
ln -s $1 $2
echo "$2 link created"
fi
}
WORKINGDIR="$ROOT_DIR/workingdir/local"
if [[ -d $WORKINGDIR ]]; then
echo "Local workingdir already prepared"
return
fi
if [[ -z "$EXPO_UNIVERSE_DIR" ]]; then
echo "EXPO_UNIVERSE_DIR needs to be set"
exit -1
fi
cd $EXPO_UNIVERSE_DIR/exponent && yarn
cd $EXPO_UNIVERSE_DIR/exponent/tools-public && yarn && ./generate-dynamic-macros-android.sh
mkdir -p $WORKINGDIR
echo "$WORKINGDIR directory created"
LINKS=("template-files" "package.json" "android" "expokit-npm-package" "packages" "client-builds" "cpp" "exponent-view-template" "ios" "shellAppBase-builds" "shellAppWorkspaces")
for LINKNAME in ${LINKS[@]}; do
ensure_link_exists "$EXPO_UNIVERSE_DIR/exponent/$LINKNAME" "$WORKINGDIR/$LINKNAME"
done
}
setup_remote() {
if [[ -d $ROOT_DIR/workingdir/android && -d $ROOT_DIR/workingdir/ios ]]; then
@@ -57,18 +55,6 @@ setup_remote() {
$ROOT_DIR/scripts/ios/fetchRemoteIosTarball.sh
fi
fi
for sdk in $ROOT_DIR/workingdir/android/* ; do
if [[ -f "$sdk/universe-package.json" ]]; then
mv $sdk/package.json $sdk/exponent-package.json
mv $sdk/universe-package.json $sdk/package.json
cd $sdk && yarn
else
if [[ -f "$sdk/package.json" ]]; then
cd $sdk && yarn
fi
fi
done
}
switch_local() {
@@ -89,10 +75,8 @@ EOF
}
if [ "$1" == "local" ]; then
# setup_local
# switch_local
echo "Setting local workingdir is currently broken, sorry :("
exit -1
setup_local
switch_local
elif [ "$1" == "remote" ]; then
setup_remote
switch_remote
+13
View File
@@ -0,0 +1,13 @@
#!/usr/bin/env bash
if [ -z "$1" ]; then
echo "Please provide an environment name as a first argument"
exit 1
fi
if [ -z "$2" ]; then
echo "Please provide a pod name as a second argument"
exit 1
fi
kubectl --namespace $1 exec -it $2 -- /bin/bash
+5
View File
@@ -0,0 +1,5 @@
#!/usr/bin/env bash
ENVIRONMENT=${1:-production}
kubectl get pods --namespace $ENVIRONMENT | grep "turtle-android-" | sed -e "s/ */ /g" | cut -d" " -f1
+5
View File
@@ -0,0 +1,5 @@
#!/usr/bin/env bash
ENVIRONMENT=${1:-production}
govc vm.ip -wait 5s turtle-js-"${ENVIRONMENT}"-*
-1
View File
@@ -21,6 +21,5 @@ export API_HOSTNAME="staging.expo.io"
export API_PORT="443"
export REDIS_URL="noop"
export REDIS_CONFIG_URL="noop"
export DATADOG_DISABLED="true"
yarn test:smoke:android
-1
View File
@@ -19,7 +19,6 @@ export API_HOSTNAME="staging.expo.io"
export API_PORT="443"
export REDIS_URL="noop"
export REDIS_CONFIG_URL="noop"
export DATADOG_DISABLED="true"
ROOT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )"/../.. && pwd )"
SDK_VERSION=sdk32 $ROOT_DIR/scripts/ios/fetchRemoteIosTarball.sh
-1
View File
@@ -21,6 +21,5 @@ export API_HOSTNAME="noop"
export API_PORT="443"
export REDIS_URL="noop"
export REDIS_CONFIG_URL="noop"
export DATADOG_DISABLED="true"
yarn test:unit
+1
View File
@@ -0,0 +1 @@
s3://exp-artifacts/android-shell-builder-c19ce0d88a153f481fd790e23bdcd6f2d5ea7fd6.tar.gz
+1 -1
View File
@@ -1 +1 @@
s3://exp-artifacts/android-shell-builder-f796aaffa0b5715079b2950faf26c2ff4e53e4c0.tar.gz
s3://exp-artifacts/android-shell-builder-0cab68670fc50ce1dc16b7d86ca7976870062c39.tar.gz
-1
View File
@@ -1 +0,0 @@
s3://exp-artifacts/android-shell-builder-aebd669d6573b0b3c802871d94cdb8b4116678d2.tar.gz
+1 -1
View File
@@ -1 +1 @@
s3://exp-artifacts/ios-expo-client-a2b1e16f330b36fa0dc627a76caabe4d2ff45f77.tar.gz
s3://exp-artifacts/ios-expo-client-15bb883c22173d5b01df19421580170e776041d0.tar.gz
+1
View File
@@ -0,0 +1 @@
s3://exp-artifacts/ios-shell-builder-sdk-26-db356a09877642d64023af98e4e2e1e632fbe4df.tar.gz
-1
View File
@@ -1 +0,0 @@
s3://exp-artifacts/ios-shell-builder-sdk-latest-a2b1e16f330b36fa0dc627a76caabe4d2ff45f77.tar.gz
+1 -1
View File
@@ -34,7 +34,7 @@ async function performSmokeTest(recording: string) {
});
}
await jobManager.doJob();
await jobManager.doJob(logger);
if (!fixtureExists) {
await fs.writeFile(recording, JSON.stringify(nock.recorder.play()));
}
-25
View File
@@ -13,28 +13,3 @@ export const TYPE_DIMENSIONS = (type: string) => [
{ Name: 'host', Value: config.hostname },
],
];
export const PRIORITY_DIMENSIONS = (type: string, priority: string) => [
[
{ Name: 'env', Value: config.deploymentEnv },
{ Name: 'platform', Value: config.platform },
{ Name: 'priority', Value: priority },
],
[
{ Name: 'env', Value: config.deploymentEnv },
{ Name: 'platform', Value: config.platform },
{ Name: 'type', Value: type },
],
[
{ Name: 'env', Value: config.deploymentEnv },
{ Name: 'platform', Value: config.platform },
{ Name: 'type', Value: type },
{ Name: 'priority', Value: priority },
],
[
{ Name: 'env', Value: config.deploymentEnv },
{ Name: 'platform', Value: config.platform },
{ Name: 'type', Value: type },
{ Name: 'host', Value: config.hostname },
],
];
+19 -18
View File
@@ -11,7 +11,6 @@ import {
MetricRegistrationObject,
} from 'turtle/aws/cloudwatch/types';
import config from 'turtle/config';
import logger from 'turtle/logger';
interface IState {
metrics: IMetric[];
@@ -38,12 +37,12 @@ const cloudWatch = new aws.CloudWatch({
secretAccessKey: config.aws.secretAccessKey,
});
export function init() {
export function init(logger: any) {
if (config.cloudwatch.disabled) {
return;
}
state.intervalId = setInterval(_dumpMetrics, config.cloudwatch.intervalMs);
state.intervalId = setInterval(_dumpMetrics(logger), config.cloudwatch.intervalMs);
}
export function addMetricData({
@@ -111,23 +110,25 @@ function _calculateGroupHash(metricName: string, extraDimensions: any[]) {
return `${metricName}${dimString}`;
}
function _dumpMetrics() {
if (!state.metrics.length && Object.keys(state.registeredMetrics).length < 0) {
// empty metrics list, skipping
return;
}
function _dumpMetrics(logger: any) {
return () => {
if (!state.metrics.length && Object.keys(state.registeredMetrics).length < 0) {
// empty metrics list, skipping
return;
}
logger.trace(
'[cloudwatch] lenghts for state.metrics=%d, state.registeredMetrics=%d',
state.metrics.length,
Object.keys(state.registeredMetrics).length,
);
logger.trace(
'[cloudwatch] lengths for state.metrics=%d, state.registeredMetrics=%d',
state.metrics.length,
Object.keys(state.registeredMetrics).length,
);
_addEmptyMetrics(state.metrics);
const formattedMetrics = _formatMetrics(state.metrics);
_pushMetrics(formattedMetrics);
_addEmptyMetrics(state.metrics);
const formattedMetrics = _formatMetrics(state.metrics);
_pushMetrics(formattedMetrics, logger);
state.metrics = [];
state.metrics = [];
};
}
function _addEmptyMetrics(metrics: IMetric[]) {
@@ -174,7 +175,7 @@ function _reduceMetrics(metrics: IMetric[]) {
);
}
function _pushMetrics(metrics: IMetricsChunk[]) {
function _pushMetrics(metrics: IMetricsChunk[], logger: any) {
logger.trace('[cloudwatch] pushing metrics', JSON.stringify(metrics, null, '\t'));
metrics.forEach((metricsChunk) => {
cloudWatch.putMetricData(metricsChunk, (err) => {
+3 -4
View File
@@ -5,7 +5,6 @@ import _ from 'lodash';
import { OUTPUT_QUEUE_URL, QUEUE_URL } from 'turtle/aws/utils';
import config from 'turtle/config';
import { JOB_UPDATE_TYPE } from 'turtle/constants/build';
import logger from 'turtle/logger';
import { getCurrentJobId } from 'turtle/turtleContext';
const sqs = new AWS.SQS({
@@ -16,7 +15,7 @@ const sqs = new AWS.SQS({
const VISIBILITY_TIMEOUT_SEC = 30;
export async function receiveMessage(priority: string) {
export async function receiveMessage(priority: string, logger: any) {
const params = {
MaxNumberOfMessages: 1,
QueueUrl: QUEUE_URL(priority),
@@ -64,7 +63,7 @@ export async function changeMessageVisibility(priority: string, receiptHandle: s
// Every VISIBILITY_TIMEOUT_SEC / 3 seconds we are telling AWS SQS
// that we're still processing the message, so it does not
// send the build job to another turtle agent
export function changeMessageVisibilityRecurring(priority: string, receiptHandle: string, jobId: string) {
export function changeMessageVisibilityRecurring(priority: string, receiptHandle: string, jobId: string, logger: any) {
return setInterval(() => {
if (getCurrentJobId() === jobId) {
changeMessageVisibility(priority, receiptHandle).catch((err) => {
@@ -74,7 +73,7 @@ export function changeMessageVisibilityRecurring(priority: string, receiptHandle
}, (VISIBILITY_TIMEOUT_SEC * 1000) / 3);
}
export async function sendMessage(jobId: string, status: JOB_UPDATE_TYPE, data = {}) {
export async function sendMessage(jobId: string, status: JOB_UPDATE_TYPE, data = {}, logger: any) {
const params = {
MessageBody: JSON.stringify({ jobId, status, ...data }),
QueueUrl: OUTPUT_QUEUE_URL(),
+18 -15
View File
@@ -1,5 +1,6 @@
import fs from 'fs-extra';
import _ from 'lodash';
import uuid from 'uuid';
import { ErrorWithCommandHelp } from 'turtle/bin/commands/ErrorWithCommandHelp';
import { createBuilderAction } from 'turtle/bin/utils/builder';
@@ -14,30 +15,18 @@ export default (program: any, setCommonCommandOptions: any) => {
.option(
'--keystore-path <app.jks>',
'path to your Keystore (please provide Keystore password and Key password as EXPO_ANDROID_KEYSTORE_PASSWORD'
+ ' and EXPO_ANDROID_KEY_PASSWORD env variables)',
+ 'and EXPO_ANDROID_KEY_PASSWORD env variables)',
)
.option('--keystore-alias <alias>', 'keystore Alias')
.option(
'-t --type <build>',
'type of build: app-bundle|apk',
/^(app-bundle|apk)$/i,
'app-bundle',
)
.option(
'-m --mode <build>',
'type of build: debug|release',
/^(debug|release)$/i,
'release',
)
.description(
'Build a standalone APK or App Bundle for your project, either signed and ready for submission to'
+ ' the Google Play Store or in debug mode.',
'Build a standalone APK for your project, signed and ready for submission to the Google Play Store.',
)
.asyncAction(
createBuilderAction({
program,
command,
prepareCredentials,
buildJobObject,
builder,
platform: PLATFORMS.ANDROID,
os: ['darwin', 'linux'],
@@ -45,6 +34,20 @@ export default (program: any, setCommonCommandOptions: any) => {
);
};
const buildJobObject = (appJSON: any, { releaseChannel, username, projectDir, publicUrl }: any, credentials: any) => ({
config: {
..._.get(appJSON, 'expo.android.config', {}),
releaseChannel,
publicUrl,
},
id: uuid.v4(),
platform: PLATFORMS.ANDROID,
sdkVersion: _.get(appJSON, 'expo.sdkVersion'),
projectDir,
experienceName: `@${username}/${_.get(appJSON, 'expo.slug')}`,
...(credentials && { credentials }),
});
const prepareCredentials = async (cmd: any) => {
const { keystorePath, keystoreAlias } = cmd;
const keystorePassword = process.env.EXPO_ANDROID_KEYSTORE_PASSWORD;
+2 -2
View File
@@ -2,8 +2,8 @@ import { default as buildAndroid } from 'turtle/bin/commands/build/android';
import { default as buildIOS } from 'turtle/bin/commands/build/ios';
function createCommand(builder: any) {
return (program: any) => {
return builder(program, setCommonCommandOptions);
return (logger: any) => (program: any) => {
return builder(program, setCommonCommandOptions, logger);
};
}
+19 -2
View File
@@ -1,10 +1,11 @@
import fs from 'fs-extra';
import _ from 'lodash';
import uuid from 'uuid';
import { ErrorWithCommandHelp } from 'turtle/bin/commands/ErrorWithCommandHelp';
import { createBuilderAction } from 'turtle/bin/utils/builder';
import builder from 'turtle/builders/ios';
import { IOS_BUILD_TYPES, PLATFORMS } from 'turtle/constants';
import { IOS, PLATFORMS } from 'turtle/constants';
export default (program: any, setCommonCommandOptions: any) => {
const command = program.command('build:ios [project-dir]');
@@ -31,6 +32,7 @@ export default (program: any, setCommonCommandOptions: any) => {
program,
command,
prepareCredentials,
buildJobObject,
builder,
platform: PLATFORMS.IOS,
os: 'darwin',
@@ -38,8 +40,23 @@ export default (program: any, setCommonCommandOptions: any) => {
);
};
const buildJobObject = (appJSON: any, { releaseChannel, buildType, username, publicUrl }: any, credentials: any) => ({
config: {
..._.get(appJSON, 'expo.ios.config', {}),
buildType,
releaseChannel,
bundleIdentifier: _.get(appJSON, 'expo.ios.bundleIdentifier'),
publicUrl,
},
id: uuid.v4(),
platform: PLATFORMS.IOS,
sdkVersion: _.get(appJSON, 'expo.sdkVersion'),
experienceName: `@${username}/${_.get(appJSON, 'expo.slug')}`,
...(credentials && { credentials }),
});
const prepareCredentials = async (cmd: any) => {
if (cmd.type !== IOS_BUILD_TYPES.ARCHIVE) {
if (cmd.type !== IOS.BUILD_TYPES.ARCHIVE) {
return null;
}
+6 -5
View File
@@ -1,8 +1,9 @@
import builders from 'turtle/bin/commands/build';
import setup from 'turtle/bin/commands/setup';
export const setupIOS = setup.ios;
export const setupAndroid = setup.android;
export const buildIOS = builders.ios;
export const buildAndroid = builders.android;
export default {
setupIOS: setup.ios,
setupAndroid: setup.android,
buildIOS: builders.ios,
buildAndroid: builders.android,
};
+4 -5
View File
@@ -4,9 +4,6 @@ import { ErrorWithCommandHelp } from 'turtle/bin/commands/ErrorWithCommandHelp';
import { ErrorWithProgramHelp } from 'turtle/bin/commands/ErrorWithProgramHelp';
import setup from 'turtle/bin/setup/setup';
import { PLATFORMS } from 'turtle/constants';
import logger from 'turtle/logger';
const l = logger.child({ buildPhase: 'setting up environment' });
function createSetupCommand(platform: string, os?: string) {
return (program: any) => {
@@ -23,14 +20,16 @@ function createSetupCommand(platform: string, os?: string) {
};
}
async function setupAction(program: any, cmd: any, platform: string, os?: string) {
async function setupAction(program: any, cmd: any, platform: string, logger: any, os?: string) {
try {
if (os && process.platform !== os) {
throw new Error('We don\'t support running standalone app builds for this platform on your operating system');
}
const { sdkVersion } = cmd;
await setup(platform, sdkVersion);
const l = logger.child({ buildPhase: 'setting up environment' });
await setup(platform, l, sdkVersion);
l.info('it\'s all set!');
} catch (err) {
logger.error({ err }, `Failed to setup environment for ${platform} builds`);
+3 -3
View File
@@ -1,10 +1,10 @@
import path from 'path';
import { LoggerDetach, ModuleVersion } from '@expo/xdl';
import program, { Command } from 'commander';
import fs from 'fs-extra';
import { LoggerDetach, ModuleVersion } from 'xdl';
import * as commands from 'turtle/bin/commands';
import commands from 'turtle/bin/commands';
import logger from 'turtle/logger';
const { name, version } = JSON.parse(fs.readFileSync(path.join(__dirname, '../../package.json'), 'utf-8'));
@@ -33,7 +33,7 @@ export function run(programName: string) {
async function runAsync(programName: string) {
program.version(version);
Object.values(commands).forEach((command) => registerCommand(program, command));
Object.values(commands).map((command) => command(logger)).forEach((command) => registerCommand(program, command));
program.parse(process.argv);
const subCommand = process.argv[2];
+24 -28
View File
@@ -1,9 +1,9 @@
import path from 'path';
import util from 'util';
import { ExponentTools } from '@expo/xdl';
import fs from 'fs-extra';
import _which from 'which';
import { ExponentTools } from 'xdl';
import ensureAndroidNDKIsPresent from 'turtle/bin/setup/android/ndk';
import ensureAndroidSDKIsPresent from 'turtle/bin/setup/android/sdk';
@@ -12,7 +12,6 @@ import { IToolDefinition } from 'turtle/bin/setup/utils/toolsDetector';
import { formatShellAppDirectory } from 'turtle/builders/utils/android/workingdir';
import config from 'turtle/config';
import { PLATFORMS } from 'turtle/constants';
import logger from 'turtle/logger';
const which = util.promisify(_which);
const REQUIRED_TOOLS: IToolDefinition[] = [
@@ -20,6 +19,10 @@ const REQUIRED_TOOLS: IToolDefinition[] = [
command: 'bash',
missingDescription: 'Please install bash',
},
{
command: 'gulp',
missingDescription: 'Run `npm install -g gulp-cli` (or `yarn global add gulp-cli`) to install gulp',
},
{
command: 'javac',
missingDescription: 'Please install JDK (version 8 or newer) - check https://jdk.java.net/',
@@ -36,25 +39,24 @@ const REQUIRED_TOOLS: IToolDefinition[] = [
},
},
];
const LOGGER_FIELDS = { buildPhase: 'setting up environment' };
const l = logger.child(LOGGER_FIELDS);
export default async function setup(sdkVersion?: string) {
await checkSystem(REQUIRED_TOOLS);
await prepareAndroidEnv();
export default async function setup(logger: any, sdkVersion?: string) {
await checkSystem(REQUIRED_TOOLS, logger);
await prepareAndroidEnv(logger);
if (sdkVersion) {
await ensureShellAppIsPresent(
sdkVersion,
{ formatShellAppDirectory, formatShellAppTarballUriPath },
logger,
_shellAppPostDownloadAction,
);
}
}
async function prepareAndroidEnv() {
async function prepareAndroidEnv(logger: any) {
await fs.ensureDir(config.directories.androidDependenciesDir);
const sdkConfig = await ensureAndroidSDKIsPresent();
const ndkConfig = await ensureAndroidNDKIsPresent();
const sdkConfig = await ensureAndroidSDKIsPresent(logger);
const ndkConfig = await ensureAndroidNDKIsPresent(logger);
_setEnvVars({ ...sdkConfig.envVars, ...ndkConfig.envVars });
_alterPath([...sdkConfig.path, ...sdkConfig.path]);
}
@@ -63,29 +65,23 @@ function formatShellAppTarballUriPath(sdkMajorVersion: string) {
return path.join(config.directories.shellTarballsDir, PLATFORMS.ANDROID, `sdk${sdkMajorVersion}`);
}
async function _shellAppPostDownloadAction(sdkVersion: string, workingdir: string) {
async function _shellAppPostDownloadAction(workingdir: string, logger: any) {
const inWorkingdir = (filename: string) => path.join(workingdir, filename);
if (await fs.pathExists(inWorkingdir('universe-package.json'))) {
// legacy shell app built from universe
await fs.move(inWorkingdir('package.json'), inWorkingdir('exponent-package.json'));
await fs.move(inWorkingdir('universe-package.json'), inWorkingdir('package.json'));
await _installNodeModules(workingdir);
await fs.move(inWorkingdir('package.json'), inWorkingdir('universe-package.json'));
await fs.move(inWorkingdir('exponent-package.json'), inWorkingdir('package.json'));
} else {
// new shell app built from expo or not
await _installNodeModules(workingdir);
}
await fs.move(inWorkingdir('package.json'), inWorkingdir('exponent-package.json'));
await fs.move(inWorkingdir('universe-package.json'), inWorkingdir('package.json'));
await _installNodeModules(workingdir, logger);
await fs.move(inWorkingdir('package.json'), inWorkingdir('universe-package.json'));
await fs.move(inWorkingdir('exponent-package.json'), inWorkingdir('package.json'));
// TODO: remove this after making sure that we don't need node_modules in tools-public for older sdks
if (ExponentTools.parseSdkMajorVersion(sdkVersion) < 33) {
const toolsPublicDir = path.join(workingdir, 'tools-public');
await _installNodeModules(toolsPublicDir);
}
// TODO: remove following lines after upgrading android shell app
const toolsPublicDir = path.join(workingdir, 'tools-public');
await _installNodeModules(toolsPublicDir, logger);
}
async function _installNodeModules(cwd: string) {
async function _installNodeModules(cwd: string, logger: any) {
const LOGGER_FIELDS = { buildPhase: 'setting up environment' };
const l = logger.child(LOGGER_FIELDS);
l.info(`installing dependencies in ${cwd} directory...`);
const command = await _shouldUseYarnOrNpm();
await ExponentTools.spawnAsyncThrowError(command, ['install'], {
+4 -9
View File
@@ -4,30 +4,25 @@ import path from 'path';
import decompress from 'decompress';
import fs from 'fs-extra';
import * as utils from 'turtle/bin/setup/utils/common';
import { formatArtifactDownloadPath } from 'turtle/bin/setup/utils/common';
import download from 'turtle/bin/setup/utils/downloader';
import config from 'turtle/config';
import logger from 'turtle/logger';
const ANDROID_NDK_URL = `https://dl.google.com/android/repository/android-ndk-r10e-${os.platform()}-x86_64.zip`;
const LOGGER_FIELDS = { buildPhase: 'setting up environment' };
const l = logger.child(LOGGER_FIELDS);
export default async function ensureAndroidNDKIsPresent() {
export default async function ensureAndroidNDKIsPresent(logger: any) {
const androidNdkDir = path.join(config.directories.androidDependenciesDir, 'ndk');
await utils.removeDirectoryUnlessReady(androidNdkDir);
if (!(await fs.pathExists(androidNdkDir))) {
await fs.ensureDir(androidNdkDir);
const androidNdkDownloadPath = utils.formatArtifactDownloadPath(ANDROID_NDK_URL);
const androidNdkDownloadPath = formatArtifactDownloadPath(ANDROID_NDK_URL);
try {
const l = logger.child({ buildPhase: 'setting up environment' });
l.info('Downloading Android NDK');
await fs.ensureDir(config.directories.artifactsDir);
await download(ANDROID_NDK_URL, androidNdkDownloadPath);
l.info('Decompressing Android NDK');
await decompress(androidNdkDownloadPath, androidNdkDir, { strip: 1 });
await utils.markDirectoryAsReady(androidNdkDir);
l.info('Android NDK installed successfully');
} catch (err) {
await fs.remove(androidNdkDir);
+6 -11
View File
@@ -1,28 +1,24 @@
import os from 'os';
import path from 'path';
import { ExponentTools } from '@expo/xdl';
import decompress from 'decompress';
import fs from 'fs-extra';
import { ExponentTools } from 'xdl';
import * as utils from 'turtle/bin/setup/utils/common';
import { formatArtifactDownloadPath } from 'turtle/bin/setup/utils/common';
import download from 'turtle/bin/setup/utils/downloader';
import config from 'turtle/config';
import logger from 'turtle/logger';
const ANDROID_SDK_URL = 'https://dl.google.com/android/repository/sdk-tools-linux-4333796.zip';
const LOGGER_FIELDS = { buildPhase: 'setting up environment' };
const l = logger.child(LOGGER_FIELDS);
export default async function ensureAndroidSDKIsPresent() {
export default async function ensureAndroidSDKIsPresent(logger: any) {
const androidSdkDir = path.join(config.directories.androidDependenciesDir, 'sdk');
await utils.removeDirectoryUnlessReady(androidSdkDir);
if (!(await fs.pathExists(androidSdkDir))) {
await fs.ensureDir(androidSdkDir);
const androidSdkDownloadPath = utils.formatArtifactDownloadPath(ANDROID_SDK_URL);
const androidSdkDownloadPath = formatArtifactDownloadPath(ANDROID_SDK_URL);
try {
const l = logger.child(LOGGER_FIELDS);
l.info('Downloading Android SDK');
await fs.ensureDir(config.directories.artifactsDir);
await download(ANDROID_SDK_URL, androidSdkDownloadPath);
@@ -32,7 +28,6 @@ export default async function ensureAndroidSDKIsPresent() {
await fs.remove(androidSdkDownloadPath);
l.info('Configuring Android SDK, this may take a while');
await _configureSdk(androidSdkDir);
await utils.markDirectoryAsReady(androidSdkDir);
l.info('Android SDK installed and configured successfully');
} catch (err) {
await fs.remove(androidSdkDir);
@@ -54,7 +49,7 @@ async function _configureSdk(androidSdkDir: string) {
await ExponentTools.spawnAsyncThrowError('./configureAndroidSdk.sh', [], {
pipeToLogger: true,
loggerFields: LOGGER_FIELDS,
cwd: path.join(config.directories.rootDir, 'scripts/android'),
cwd: path.join(config.directories.rootDir, 'scripts/cli'),
env: {
...process.env,
ANDROID_HOME: androidSdkDir,
+4 -30
View File
@@ -1,9 +1,6 @@
import last from 'lodash/last';
import trim from 'lodash/trim';
import path from 'path';
import semver from 'semver';
import { ExponentTools } from '@expo/xdl';
import { ExponentTools } from 'xdl';
import { checkSystem, ensureShellAppIsPresent } from 'turtle/bin/setup/utils/common';
import { IToolDefinition } from 'turtle/bin/setup/utils/toolsDetector';
@@ -19,21 +16,6 @@ const REQUIRED_TOOLS: IToolDefinition[] = [
{
command: 'fastlane',
missingDescription: 'Please check https://docs.fastlane.tools/getting-started/ios/setup/',
versionCheckFn: async () => {
const MINIMAL_VERSION = '2.99.0';
const { stdout } = await ExponentTools.spawnAsyncThrowError(
'fastlane',
['--version'],
{ stdio: 'pipe', env: { ...process.env, FASTLANE_SKIP_UPDATE_CHECK: '1', LC_ALL: 'en_US.UTF-8' } },
);
const fastlaneVersion = parseFastlaneVersion(stdout);
if (fastlaneVersion && !semver.satisfies(fastlaneVersion, `>= ${MINIMAL_VERSION}`)) {
throw new Error(
`Your fastlane on version ${fastlaneVersion}. Please upgrade it to at least ${MINIMAL_VERSION}.`,
);
}
},
},
{
command: 'xcode',
@@ -52,24 +34,16 @@ const REQUIRED_TOOLS: IToolDefinition[] = [
},
];
export default async function setup(sdkVersion?: string) {
await checkSystem(REQUIRED_TOOLS);
export default async function setup(logger: any, sdkVersion?: string) {
await checkSystem(REQUIRED_TOOLS, logger);
if (sdkVersion) {
await ensureShellAppIsPresent(sdkVersion, {
formatShellAppDirectory,
formatShellAppTarballUriPath,
});
}, logger);
}
}
function formatShellAppTarballUriPath(sdkMajorVersion: string) {
return path.join(config.directories.shellTarballsDir, PLATFORMS.IOS, `sdk${sdkMajorVersion}`);
}
function parseFastlaneVersion(stdout: string) {
const lastLine = last(trim(stdout).split('\n'));
if (!lastLine) {
return null;
}
return last(lastLine.split(' ')) || null;
}
+3 -3
View File
@@ -4,15 +4,15 @@ import setupAndroid from 'turtle/bin/setup/android';
import setupIos from 'turtle/bin/setup/ios';
import { PLATFORMS } from 'turtle/constants';
export default async function setup(platform: string, sdkVersion?: string) {
export default async function setup(platform: string, logger: any, sdkVersion?: string) {
if (sdkVersion && !isSemver(sdkVersion)) {
throw new Error('SDK version is not valid.');
}
if (platform === PLATFORMS.IOS) {
return await setupIos(sdkVersion);
return await setupIos(logger, sdkVersion);
} else if (platform === PLATFORMS.ANDROID) {
return await setupAndroid(sdkVersion);
return await setupAndroid(logger, sdkVersion);
} else {
throw new Error('This should never happen :(');
}
+17 -44
View File
@@ -1,60 +1,61 @@
import path from 'path';
import { ExponentTools } from '@expo/xdl';
import fs from 'fs-extra';
import tar from 'tar';
import { ExponentTools } from 'xdl';
import download from 'turtle/bin/setup/utils/downloader';
import { ensureToolsAreInstalled, IToolDefinition } from 'turtle/bin/setup/utils/toolsDetector';
import { IShellAppDirectoryConfig } from 'turtle/builders/utils/workingdir';
import config from 'turtle/config';
import logger from 'turtle/logger';
const l = logger.child({ buildPhase: 'setting up environment' });
interface IShellAppFormaters {
formatShellAppDirectory: (config: IShellAppDirectoryConfig) => string;
formatShellAppTarballUriPath: (sdkMajorVersion: string) => string;
}
type PostDownloadAction = (sdkVersion: string, workingdir: string) => void;
type PostDownloadAction = (workingdir: string, logger: any) => void;
export async function checkSystem(requiredTools: IToolDefinition[]) {
await ensureToolsAreInstalled(requiredTools);
export async function checkSystem(requiredTools: IToolDefinition[], logger: any) {
await ensureToolsAreInstalled(requiredTools, logger);
}
export async function ensureShellAppIsPresent(
sdkVersion: string,
formatters: IShellAppFormaters,
logger: any,
postDownloadAction?: PostDownloadAction,
) {
const workingdir = formatters.formatShellAppDirectory({ sdkVersion });
const shellAppMetadata = await _readShellAppTarballS3Uri(sdkVersion, formatters);
await removeDirectoryUnlessReady(workingdir, shellAppMetadata);
const l = logger.child({ buildPhase: 'setting up environment' });
if (await fs.pathExists(workingdir)) {
return;
}
l.info(`shell app for SDK ${sdkVersion} doesn't exist, downloading...`);
await _downloadShellApp(sdkVersion, workingdir, formatters);
await _downloadShellApp(sdkVersion, workingdir, formatters, l);
if (postDownloadAction) {
await postDownloadAction(sdkVersion, workingdir);
await postDownloadAction(workingdir, logger);
}
await markDirectoryAsReady(workingdir, shellAppMetadata);
}
async function _downloadShellApp(sdkVersion: string, targetDirectory: string, formatters: IShellAppFormaters) {
async function _downloadShellApp(
sdkVersion: string,
targetDirectory: string,
formatters: IShellAppFormaters,
logger: any,
) {
const shellAppTarballS3Uri = await _readShellAppTarballS3Uri(sdkVersion, formatters);
const tarballDownloadTargetPath = formatArtifactDownloadPath(shellAppTarballS3Uri);
await fs.ensureDir(config.directories.artifactsDir);
await download(shellAppTarballS3Uri, tarballDownloadTargetPath);
l.info('shell app has been downloaded');
logger.info('shell app has been downloaded');
await fs.ensureDir(targetDirectory);
l.info('extracting shell app (this may take a while)...');
logger.info('extracting shell app (this may take a while)...');
await tar.x({
file: tarballDownloadTargetPath,
C: targetDirectory,
});
l.info('shell app extracted');
logger.info('shell app extracted');
}
export function formatArtifactDownloadPath(uri: string) {
@@ -68,31 +69,3 @@ async function _readShellAppTarballS3Uri(sdkVersion: string, formatters: IShellA
const data = await fs.readFile(filePath, 'utf8');
return data.trim();
}
export async function removeDirectoryUnlessReady(dir: string, metadata?: string) {
const readyFilePath = path.join(dir, '.ready');
const readyFileExists = await fs.pathExists(readyFilePath);
let shouldRemove = false;
if (readyFileExists) {
if (metadata !== undefined) {
const readyFileContents = (await fs.readFile(readyFilePath, 'utf-8')).trim();
if (readyFileContents !== metadata) {
shouldRemove = true;
}
}
} else {
shouldRemove = true;
}
if (shouldRemove) {
await fs.remove(dir);
}
}
export async function markDirectoryAsReady(dir: string, metadata?: string) {
const readyFilePath = path.join(dir, '.ready');
if (metadata === undefined) {
await fs.open(readyFilePath, 'w');
} else {
await fs.writeFile(readyFilePath, metadata);
}
}
+3 -14
View File
@@ -1,21 +1,17 @@
import util from 'util';
import _which from 'which';
import logger from 'turtle/logger';
const which = util.promisify(_which);
const l = logger.child({ buildPhase: 'setting up environment' });
export interface IToolDefinition {
command: string;
missingDescription: string;
testFn?: () => Promise<boolean>;
versionCheckFn?: () => void;
}
export async function ensureToolsAreInstalled(tools: IToolDefinition[]) {
export async function ensureToolsAreInstalled(tools: IToolDefinition[], logger: any) {
let isAnyToolMissing = false;
for (const { command, missingDescription, testFn, versionCheckFn } of tools) {
for (const { command, missingDescription, testFn } of tools) {
try {
if (testFn) {
if (!await testFn()) {
@@ -25,20 +21,13 @@ export async function ensureToolsAreInstalled(tools: IToolDefinition[]) {
await which(command);
}
} catch (err) {
const l = logger.child({ buildPhase: 'setting up environment' });
isAnyToolMissing = true;
if (!testFn) {
l.error({ err }, `${command} is missing in your $PATH`);
}
l.error({ err }, missingDescription);
}
if (versionCheckFn) {
try {
await versionCheckFn();
} catch (err) {
isAnyToolMissing = true;
l.error({ err }, `wrong version of ${command} installed`);
}
}
}
if (isAnyToolMissing) {
throw new Error('Tools required to perform standalone app build are missing. Please install them.');
+4 -42
View File
@@ -1,20 +1,18 @@
import { ExponentTools } from '@expo/xdl';
import _ from 'lodash';
import uuid from 'uuid';
import { ErrorWithCommandHelp } from 'turtle/bin/commands/ErrorWithCommandHelp';
import { ErrorWithProgramHelp } from 'turtle/bin/commands/ErrorWithProgramHelp';
import setup from 'turtle/bin/setup/setup';
import * as ProjectUtils from 'turtle/bin/utils/project';
import * as UserUtils from 'turtle/bin/utils/user';
import { getExperienceUrl } from 'turtle/builders/utils/common';
import logger from 'turtle/logger';
import { sanitizeJob } from 'turtle/validator';
export function createBuilderAction({
logger,
program,
command,
prepareCredentials,
buildJobObject,
builder,
platform,
os,
@@ -57,7 +55,6 @@ export function createBuilderAction({
const args = {
releaseChannel: cmd.releaseChannel || 'default',
buildType: cmd.type,
buildMode: cmd.mode,
username: userData.username || 'anonymous',
projectDir: ProjectUtils.resolveAbsoluteDir(projectDirArg),
publicUrl: cmd.publicUrl,
@@ -65,10 +62,10 @@ export function createBuilderAction({
const appJSON = await ProjectUtils.loadAppJSON(projectDirArg, cmd.config);
const sdkVersion = _.get(appJSON, 'expo.sdkVersion');
await setup(platform, sdkVersion);
await setup(platform, logger, sdkVersion);
const credentials = await prepareCredentials(cmd);
const rawJob = {
...await buildJobObject(platform, appJSON, args, credentials),
...buildJobObject(appJSON, args, credentials),
...cmd.buildDir && { fakeUploadDir: ProjectUtils.resolveAbsoluteDir(cmd.buildDir) },
...cmd.output && { fakeUploadBuildPath: ProjectUtils.resolveAbsoluteDir(cmd.output) },
};
@@ -85,38 +82,3 @@ export function createBuilderAction({
}
};
}
const buildJobObject = async (
platform: 'android' | 'ios',
appJSON: any,
{ releaseChannel, buildType, buildMode, username, publicUrl, projectDir }: any,
credentials: any,
) => {
const job = {
config: {
..._.get(appJSON, 'expo.ios.config', {}),
buildType,
...(platform === 'android' ? { buildMode } : {}),
releaseChannel,
...(platform === 'ios' ? { bundleIdentifier: _.get(appJSON, 'expo.ios.bundleIdentifier') } : {}),
...(platform === 'android' ? { androidPackage: _.get(appJSON, 'expo.android.package') } : {}),
publicUrl,
},
id: uuid.v4(),
platform,
projectDir,
sdkVersion: _.get(appJSON, 'expo.sdkVersion'),
experienceName: `@${username}/${_.get(appJSON, 'expo.slug')}`,
...(credentials && { credentials }),
};
const url = getExperienceUrl(job.experienceName, job.config.publicUrl);
const manifest = await ExponentTools.getManifestAsync(url, {
'Exponent-SDK-Version': _.get(appJSON, 'expo.sdkVersion'),
'Exponent-Platform': platform,
'Expo-Release-Channel': releaseChannel,
'Accept': 'application/expo+json,application/json',
});
return { ...job, manifest };
};
+1 -1
View File
@@ -1,4 +1,4 @@
import { User as UserManager } from '@expo/xdl';
import { User as UserManager } from 'xdl';
import { ErrorWithProgramHelp } from 'turtle/bin/commands/ErrorWithProgramHelp';
+11 -25
View File
@@ -1,36 +1,30 @@
import path from 'path';
import { AndroidShellApp, ImageUtils } from '@expo/xdl';
import fs from 'fs-extra';
import _ from 'lodash';
import semver from 'semver';
import { ANDROID_BUILD_TYPES } from 'turtle/constants';
import uuidv4 from 'uuid/v4';
import { AndroidShellApp, ImageUtils } from 'xdl';
import getOrCreateCredentials from 'turtle/builders/utils/android/credentials';
import { formatShellAppDirectory } from 'turtle/builders/utils/android/workingdir';
import * as commonUtils from 'turtle/builders/utils/common';
import * as imageHelpers from 'turtle/builders/utils/image';
import { resolveNativeModules } from 'turtle/builders/utils/unimodules';
import { uploadBuildToS3 } from 'turtle/builders/utils/uploader';
import { ensureCanBuildSdkVersion } from 'turtle/builders/utils/version';
import config from 'turtle/config';
import { IAndroidCredentials, IJob, IJobResult } from 'turtle/job';
import logger from 'turtle/logger';
export default async function buildAndroid(jobData: IJob): Promise<IJobResult> {
export default async function buildAndroid(jobData: IJob, logger: any): Promise<IJobResult> {
await ensureCanBuildSdkVersion(jobData);
const credentials = await getOrCreateCredentials(jobData);
const outputFilePath = await runShellAppBuilder(jobData, credentials);
const credentials = await getOrCreateCredentials(jobData, logger);
const apkFilePath = await runShellAppBuilder(jobData, credentials, logger);
const randomHex = uuidv4().replace(/-/g, '');
const s3FileExtension = jobData.config.buildType === ANDROID_BUILD_TYPES.APP_BUNDLE ? 'aab' : 'apk';
const s3Filename = `${jobData.experienceName}-${randomHex}-signed.${s3FileExtension}`;
const s3Filename = `${jobData.experienceName}-${randomHex}-signed.apk`;
const s3FileKey = `android/${s3Filename}`;
const fakeUploadFilename = s3Filename.replace('/', '\\');
const artifactUrl = await uploadBuildToS3({
uploadPath: outputFilePath,
logger,
uploadPath: apkFilePath,
s3FileKey,
...config.builder.fakeUpload && {
fakeUploadBuildPath:
@@ -46,6 +40,7 @@ export default async function buildAndroid(jobData: IJob): Promise<IJobResult> {
async function runShellAppBuilder(
jobData: IJob,
credentials: IAndroidCredentials,
logger: any,
): Promise<string> {
const { temporaryFilesRoot } = config.directories;
await fs.ensureDir(temporaryFilesRoot);
@@ -66,21 +61,15 @@ async function runShellAppBuilder(
ImageUtils.setResizeImageFunction(imageHelpers.resizeIconWithSharpAsync);
ImageUtils.setGetImageDimensionsFunction(imageHelpers.getImageDimensionsWithSharpAsync);
const fileExtension = jobData.config.buildType === ANDROID_BUILD_TYPES.APP_BUNDLE ? 'aab' : 'apk';
const outputFilePath = path.join(temporaryFilesRoot, `shell-signed-${jobData.id}.${fileExtension}`);
const outputFilePath = path.join(temporaryFilesRoot, `shell-signed-${jobData.id}.apk`);
const { config: jobConfig, manifest, sdkVersion: sdkVersionFromJob } = jobData;
const sdkVersion = _.get(manifest, 'sdkVersion', sdkVersionFromJob);
const workingDir = formatShellAppDirectory({ sdkVersion });
logger.info({ buildPhase: 'resolve native modules' }, 'Resolving universal modules dependencies');
const enabledModules = semver.satisfies(sdkVersion, '>= 33.0.0')
? await resolveNativeModules(workingDir, _.get(manifest, 'dependencies'))
: null;
try {
await AndroidShellApp.createAndroidShellAppAsync({
url: commonUtils.getExperienceUrl(jobData.experienceName, jobData.config.publicUrl),
url: commonUtils.getExperienceUrl(jobData),
sdkVersion,
keystore: tempKeystorePath,
manifest,
@@ -91,12 +80,9 @@ async function runShellAppBuilder(
releaseChannel: jobConfig.releaseChannel,
workingDir,
outputFile: outputFilePath,
modules: enabledModules,
buildType: jobData.config.buildType,
buildMode: jobData.config.buildMode,
});
} catch (err) {
commonUtils.logErrorOnce(err);
commonUtils.logErrorOnce(err, logger);
throw err;
} finally {
if (!config.builder.skipCleanup) {
+4 -1
View File
@@ -1,7 +1,10 @@
import { IJob, IJobResult } from 'turtle/job';
import buildAndroid from './android';
import buildIOS from './ios';
export default {
const builders: { [index: string]: (job: IJob, logger: any) => Promise<IJobResult>} = {
ios: buildIOS,
android: buildAndroid,
};
export default builders;
+6 -7
View File
@@ -1,5 +1,5 @@
import { IosIPABuilder as createIPABuilder } from '@expo/xdl';
import _ from 'lodash';
import { IosIPABuilder as createIPABuilder } from 'xdl';
import { IContext } from 'turtle/builders/ios/context';
import { logErrorOnce } from 'turtle/builders/utils/common';
@@ -7,7 +7,6 @@ import * as fileUtils from 'turtle/builders/utils/file';
import * as keychain from 'turtle/builders/utils/ios/keychain';
import runShellAppBuilder from 'turtle/builders/utils/ios/shellAppBuilder';
import { IJob } from 'turtle/job';
import logger from 'turtle/logger/index';
export default async function buildArchive(ctx: IContext, job: IJob) {
let keychainInfo;
@@ -19,7 +18,7 @@ export default async function buildArchive(ctx: IContext, job: IJob) {
const manifest = await runShellAppBuilder(ctx, job);
await buildAndSignIPA(ctx, job, keychainInfo.path, manifest);
} catch (err) {
logErrorOnce(err);
logErrorOnce(err, ctx.logger);
throw err;
} finally {
if (keychainInfo) {
@@ -29,8 +28,8 @@ export default async function buildArchive(ctx: IContext, job: IJob) {
}
async function buildAndSignIPA(ctx: IContext, job: IJob, keychainPath: string, manifest: any) {
const l = logger.child({ buildPhase: 'building and signing IPA' });
l.info('building and signing IPA');
ctx.logger = ctx.logger.child({ buildPhase: 'building and signing IPA' });
ctx.logger.info('building and signing IPA');
const {
credentials: { provisioningProfile, certPassword, teamId },
@@ -42,7 +41,7 @@ async function buildAndSignIPA(ctx: IContext, job: IJob, keychainPath: string, m
const { provisioningProfilePath } = ctx;
await fileUtils.writeBase64ToBinaryFile(provisioningProfilePath, provisioningProfile as string);
l.info('saved provisioning profile to temporary path');
ctx.logger.info('saved provisioning profile to temporary path');
const ipaBuilder = createIPABuilder({
keychainPath,
@@ -57,5 +56,5 @@ async function buildAndSignIPA(ctx: IContext, job: IJob, keychainPath: string, m
});
await ipaBuilder.build();
l.info(`done building and signing IPA`);
ctx.logger.info(`done building and signing IPA`);
}
+11 -8
View File
@@ -1,11 +1,11 @@
import * as path from 'path';
import { IosShellApp } from '@expo/xdl';
import { v4 as uuidv4 } from 'uuid';
import { IosShellApp } from 'xdl';
import { formatShellAppDirectory } from 'turtle/builders/utils/ios/workingdir';
import config from 'turtle/config';
import { IOS_BUILD_TYPES, PLATFORMS } from 'turtle/constants/index';
import { IOS, PLATFORMS } from 'turtle/constants/index';
import { IJob } from 'turtle/job';
export interface IContext {
@@ -15,6 +15,7 @@ export interface IContext {
baseArchiveDir: string;
buildDir: string;
fakeUploadBuildPath?: string;
logger: any;
outputPath: string;
provisioningProfilePath: string;
provisioningProfileDir: string;
@@ -27,8 +28,9 @@ export interface IContext {
}
const { EXPOKIT_APP, EXPONENT_APP } = IosShellApp;
const { BUILD_TYPES } = IOS;
export function createBuilderContext(job: IJob): IContext {
export function createBuilderContext(job: IJob, logger: any): IContext {
const { join } = path;
const appUUID = uuidv4();
const {
@@ -46,6 +48,7 @@ export function createBuilderContext(job: IJob): IContext {
appDir: join(config.directories.temporaryFilesRoot, appUUID),
appUUID,
workingDir,
logger,
};
context.buildDir = join(context.appDir, 'build');
context.provisioningProfileDir = join(context.appDir, 'provisioning');
@@ -56,7 +59,7 @@ export function createBuilderContext(job: IJob): IContext {
context.tempCertPath = join(context.appDir, 'cert.p12');
context.baseArchiveDir = join(context.appDir, 'archive');
if (buildType === IOS_BUILD_TYPES.CLIENT) {
if (buildType === BUILD_TYPES.CLIENT) {
context.applicationFilesSrc = join(
workingDir,
'expo-client-build',
@@ -73,7 +76,7 @@ export function createBuilderContext(job: IJob): IContext {
);
}
if (buildType === IOS_BUILD_TYPES.ARCHIVE) {
if (buildType === BUILD_TYPES.ARCHIVE) {
context.outputPath = join(context.appDir, 'archive.xcarchive');
context.uploadPath = join(context.buildDir, 'archive.ipa');
context.archiveDir = join(
@@ -91,7 +94,7 @@ export function createBuilderContext(job: IJob): IContext {
'default',
`${EXPOKIT_APP}.xcworkspace`,
);
} else if (buildType === IOS_BUILD_TYPES.CLIENT) {
} else if (buildType === BUILD_TYPES.CLIENT) {
context.outputPath = join(context.appDir, 'archive.xcarchive');
context.uploadPath = join(context.buildDir, 'archive.ipa');
context.archiveDir = join(
@@ -106,7 +109,7 @@ export function createBuilderContext(job: IJob): IContext {
'ios',
`${EXPONENT_APP}.xcworkspace`,
);
} else if (buildType === IOS_BUILD_TYPES.SIMULATOR) {
} else if (buildType === BUILD_TYPES.SIMULATOR) {
context.outputPath = join(context.appDir, 'archive.tar.gz');
context.uploadPath = join(context.appDir, 'archive.tar.gz');
context.archiveDir = join(
@@ -116,7 +119,7 @@ export function createBuilderContext(job: IJob): IContext {
);
}
const s3FileExtension = buildType === IOS_BUILD_TYPES.SIMULATOR ? 'tar.gz' : 'ipa';
const s3FileExtension = buildType === BUILD_TYPES.SIMULATOR ? 'tar.gz' : 'ipa';
const s3Filename = `${job.experienceName}-${appUUID}-${buildType}.${s3FileExtension}`;
if (config.builder.fakeUpload) {
const fakeUploadFilename = s3Filename.replace('/', '\\');
+11 -9
View File
@@ -9,28 +9,30 @@ import prepareAdHocBuildCredentials from 'turtle/builders/utils/ios/adhocBuild';
import { uploadBuildToS3 } from 'turtle/builders/utils/uploader';
import { ensureCanBuildSdkVersion } from 'turtle/builders/utils/version';
import config from 'turtle/config';
import { IOS_BUILD_TYPES } from 'turtle/constants';
import { IOS } from 'turtle/constants/index';
import { IJob, IJobResult } from 'turtle/job';
export default async function iosBuilder(job: IJob): Promise<IJobResult> {
if (job.config.buildType !== IOS_BUILD_TYPES.CLIENT) {
const { BUILD_TYPES } = IOS;
export default async function iosBuilder(job: IJob, logger: any): Promise<IJobResult> {
if (job.config.buildType !== BUILD_TYPES.CLIENT) {
await ensureCanBuildSdkVersion(job);
}
const ctx = createBuilderContext(job);
const ctx = createBuilderContext(job, logger);
try {
await initBuilder(ctx);
const { buildType } = job.config;
if (buildType === IOS_BUILD_TYPES.CLIENT) {
await prepareAdHocBuildCredentials(job);
if (buildType === BUILD_TYPES.CLIENT) {
await prepareAdHocBuildCredentials(job, ctx.logger);
}
if ([IOS_BUILD_TYPES.ARCHIVE, IOS_BUILD_TYPES.CLIENT].includes(buildType!)) {
if ([BUILD_TYPES.ARCHIVE, BUILD_TYPES.CLIENT].includes(buildType!)) {
await buildArchive(ctx, job);
} else if (buildType === IOS_BUILD_TYPES.SIMULATOR) {
} else if (buildType === BUILD_TYPES.SIMULATOR) {
await buildSimulator(ctx, job);
} else {
throw new Error(`Unsupported iOS build type: ${buildType}`);
@@ -39,7 +41,7 @@ export default async function iosBuilder(job: IJob): Promise<IJobResult> {
const artifactUrl = await uploadBuildToS3(ctx);
return { artifactUrl };
} catch (err) {
logErrorOnce(err);
logErrorOnce(err, ctx.logger);
throw err;
} finally {
await cleanup(ctx);
+3 -4
View File
@@ -1,11 +1,10 @@
import { IContext } from 'turtle/builders/ios/context';
import runShellAppBuilder from 'turtle/builders/utils/ios/shellAppBuilder';
import { IJob } from 'turtle/job';
import logger from 'turtle/logger';
export default async function buildSimulator(ctx: IContext, job: IJob) {
const l = logger.child({ buildPhase: 'running simulator builder' });
l.info('running simulator build');
ctx.logger = ctx.logger.child({ buildPhase: 'running simulator builder' });
ctx.logger.info('running simulator build');
await runShellAppBuilder(ctx, job);
l.info('build complete');
ctx.logger.info('build complete');
}
+4 -13
View File
@@ -1,17 +1,15 @@
import path from 'path';
import { Credentials } from '@expo/xdl';
import fs from 'fs-extra';
import get from 'lodash/get';
import uuidv4 from 'uuid/v4';
import { Credentials } from 'xdl';
import * as sqs from 'turtle/aws/sqs';
import { UPDATE_CREDENTIALS } from 'turtle/constants/build';
import { IAndroidCredentials, IJob } from 'turtle/job';
import logger from 'turtle/logger';
import { isOffline } from 'turtle/turtleContext';
async function getOrCreateCredentials(jobData: IJob): Promise<IAndroidCredentials> {
async function getOrCreateCredentials(jobData: IJob, logger: any): Promise<IAndroidCredentials> {
if (!jobData.credentials) {
const l = logger.child({ buildPhase: 'generating keystore' });
const credentials: any = {};
@@ -20,14 +18,7 @@ async function getOrCreateCredentials(jobData: IJob): Promise<IAndroidCredential
credentials.keystorePassword = uuidv4().replace(/-/g, '');
credentials.keyPassword = uuidv4().replace(/-/g, '');
credentials.keystoreAlias = Buffer.from(jobData.experienceName).toString('base64');
const androidPackage =
get(jobData, 'manifest.android.package')
|| get(jobData, 'config.androidPackage');
if (!androidPackage) {
throw new Error(
'Android package name is not set in the app manifest (please update app.json if you\'re using turtle-cli).',
);
}
const androidPackage = jobData.manifest.android.package;
await Credentials.Android.createKeystore(
{
@@ -52,7 +43,7 @@ async function getOrCreateCredentials(jobData: IJob): Promise<IAndroidCredential
} else {
await sqs.sendMessage(jobData.id, UPDATE_CREDENTIALS, {
...credentials,
});
}, l);
l.info('Keystore sent to storage successfully');
}
await fs.unlink(keystoreFilename);
+1 -1
View File
@@ -1,6 +1,6 @@
import path from 'path';
import { ExponentTools } from '@expo/xdl';
import { ExponentTools } from 'xdl';
import { IShellAppDirectoryConfig } from 'turtle/builders/utils/workingdir';
import config from 'turtle/config';
+6 -3
View File
@@ -1,9 +1,12 @@
import * as url from 'url';
import config from 'turtle/config';
import logger from 'turtle/logger';
import { IJob } from 'turtle/job';
export function getExperienceUrl(job: IJob) {
const { experienceName, config: jobConfig } = job;
const { publicUrl } = jobConfig;
export function getExperienceUrl(experienceName: string, publicUrl?: string) {
// publicUrl is passed in if user wants to build an externally hosted app
if (publicUrl) {
return publicUrl;
@@ -14,7 +17,7 @@ export function getExperienceUrl(experienceName: string, publicUrl?: string) {
const alreadyLoggedError = Symbol('alreadyLoggedError');
export function logErrorOnce(err: any) {
export function logErrorOnce(err: any, logger: any) {
if (!err[alreadyLoggedError]) {
logger.error(err.stack);
err[alreadyLoggedError] = true;
+15 -24
View File
@@ -1,37 +1,29 @@
import path from 'path';
import spawnAsync from '@expo/spawn-async';
import { IosCodeSigning } from '@expo/xdl';
import fs from 'fs-extra';
import isEmpty from 'lodash/isEmpty';
import { IosCodeSigning } from 'xdl';
import * as sqs from 'turtle/aws/sqs';
import BuildError, { BuildErrorReason } from 'turtle/builders/BuildError';
import { UPDATE_CREDENTIALS } from 'turtle/constants/build';
import { IJob } from 'turtle/job';
import logger from 'turtle/logger';
import { isOffline } from 'turtle/turtleContext';
// tslint:disable-next-line:no-var-requires
const travelingFastlane = process.platform === 'darwin' ? require('@expo/traveling-fastlane-darwin')() : null;
async function prepareAdHocBuildCredentials(job: IJob) {
async function prepareAdHocBuildCredentials(job: IJob, logger: any) {
if (process.platform !== 'darwin') {
throw new Error('This function should be called only on macOS!');
}
const { bundleIdentifier } = job.config;
const {
certP12,
certPassword,
teamId,
appleSession,
udids,
provisioningProfileId,
} = job.credentials;
const { certP12, certPassword, teamId, appleSession, udids } = job.credentials;
const certSerialNumber = IosCodeSigning.findP12CertSerialNumber(certP12, certPassword);
const args = [
...(provisioningProfileId ? ['--profile-id', provisioningProfileId] : []),
teamId,
udids!.join(','),
bundleIdentifier,
@@ -39,26 +31,25 @@ async function prepareAdHocBuildCredentials(job: IJob) {
];
try {
if (provisioningProfileId) {
logger.info(
`Using ad hoc provisioning profile id: ${provisioningProfileId}`,
);
}
const credentials = await runFastlaneAction(
travelingFastlane.manageAdHocProvisioningProfile,
args,
{ env: { FASTLANE_SESSION: appleSession } },
);
if (isEmpty(credentials)) {
if (!job.credentials.provisioningProfile) {
// a user has generated the provisioning profile for his app, but he probably deleted it from Expo servers, ...
throw new BuildError(
BuildErrorReason.PROVISIONING_PROFILE_MISSING,
`Provisioning profile is missing on Expo servers.`,
);
}
return;
}
logger.info('New ad hoc provisioning profile successfully created');
job.credentials.provisioningProfile = credentials.provisioningProfile;
if (!credentials.provisioningProfileUpdateTimestamp) {
// dont need to persist profile because nothing changed
return;
}
if (isOffline()) {
const provisioningProfilePath = path.join(job.projectDir, `adhoc.mobileprovision`);
await fs.writeFile(provisioningProfilePath, Buffer.from(credentials.provisioningProfile, 'base64'));
@@ -67,7 +58,7 @@ async function prepareAdHocBuildCredentials(job: IJob) {
await sqs.sendMessage(job.id, UPDATE_CREDENTIALS, {
provisioningProfileId: credentials.provisioningProfileId,
provisioningProfile: credentials.provisioningProfile,
});
}, logger);
logger.info('Ad Hoc provisioning profile sent to storage successfully');
}
} catch (e) {
+4 -5
View File
@@ -1,15 +1,14 @@
import { IosKeychain } from '@expo/xdl';
import { IosKeychain } from 'xdl';
import { IContext } from 'turtle/builders/ios/context';
import * as fileUtils from 'turtle/builders/utils/file';
import logger from 'turtle/logger';
interface IKeychain {
path: string;
}
export async function create(ctx: IContext): Promise<IKeychain> {
const l = logger.child({ buildPhase: 'creating keychain' });
const l = ctx.logger.child({ buildPhase: 'creating keychain' });
try {
l.info('creating keychain...');
const keychainInfo = await IosKeychain.createKeychain(ctx.appUUID, false);
@@ -22,7 +21,7 @@ export async function create(ctx: IContext): Promise<IKeychain> {
}
export async function remove(ctx: IContext, keychainPath: string) {
const l = logger.child({ buildPhase: 'deleting keychain' });
const l = ctx.logger.child({ buildPhase: 'deleting keychain' });
try {
l.info('delete keychain...');
const keychainInfo = await IosKeychain.deleteKeychain({
@@ -49,7 +48,7 @@ export async function importCert(
certPassword,
}: { keychainPath: string; certP12: string; certPassword: string },
) {
const l = logger.child({ buildPhase: 'importing certificate into keychain' });
const l = ctx.logger.child({ buildPhase: 'importing certificate into keychain' });
try {
l.info('importing distribution certificate into keychain...');
const { tempCertPath: certPath } = ctx;
+6 -7
View File
@@ -1,16 +1,15 @@
import path from 'path';
import util from 'util';
import { ImageUtils, IosShellApp } from '@expo/xdl';
import copy from 'copy';
import _ from 'lodash';
import { ImageUtils, IosShellApp } from 'xdl';
import { IContext } from 'turtle/builders/ios/context';
import * as commonUtils from 'turtle/builders/utils/common';
import * as imageHelpers from 'turtle/builders/utils/image';
import { IOS_BUILD_TYPES } from 'turtle/constants/index';
import { IOS } from 'turtle/constants/index';
import { IJob } from 'turtle/job';
import logger from 'turtle/logger/index';
const copyAsync = util.promisify(copy);
@@ -21,7 +20,7 @@ export default async function runShellAppBuilder(ctx: IContext, job: IJob): Prom
await copyAsync(ctx.applicationFilesSrc, ctx.baseArchiveDir);
logger.info(
ctx.logger.info(
{ buildPhase: 'icons setup' },
'ImageUtils: setting image functions to alternative sharp implementations',
);
@@ -37,18 +36,18 @@ export default async function runShellAppBuilder(ctx: IContext, job: IJob): Prom
output: ctx.outputPath,
verbose: true,
};
if (buildType === IOS_BUILD_TYPES.CLIENT) {
if (buildType === IOS.BUILD_TYPES.CLIENT) {
Object.assign(shellAppParams, {
workspacePath: path.join(ctx.workingDir, 'ios'),
});
} else {
Object.assign(shellAppParams, {
url: commonUtils.getExperienceUrl(job.experienceName, job.config.publicUrl),
url: commonUtils.getExperienceUrl(job),
releaseChannel,
sdkVersion,
});
}
logger.info({ buildPhase: 'configuring NSBundle' }, 'configuring NSBundle...');
ctx.logger.info({ buildPhase: 'configuring NSBundle' }, 'configuring NSBundle...');
return await IosShellApp.configureAndCopyArchiveAsync(shellAppParams);
}
+3 -3
View File
@@ -1,14 +1,14 @@
import path from 'path';
import { ExponentTools } from '@expo/xdl';
import { ExponentTools } from 'xdl';
import { IShellAppDirectoryConfig } from 'turtle/builders/utils/workingdir';
import config from 'turtle/config';
import { IOS_BUILD_TYPES, PLATFORMS } from 'turtle/constants';
import { IOS, PLATFORMS } from 'turtle/constants';
export function formatShellAppDirectory({ sdkVersion, buildType }: IShellAppDirectoryConfig) {
const { workingDir } = config.directories;
if (buildType === IOS_BUILD_TYPES.CLIENT) {
if (buildType === IOS.BUILD_TYPES.CLIENT) {
return path.join(workingDir, PLATFORMS.IOS, 'client');
} else if (config.builder.useLocalWorkingDir) {
return path.join(workingDir, 'local');
-169
View File
@@ -1,169 +0,0 @@
import fs from 'fs-extra';
import filter from 'lodash/filter';
import keys from 'lodash/keys';
import merge from 'lodash/merge';
import values from 'lodash/values';
import path from 'path';
import logger from 'turtle/logger';
export interface IUnimoduleEntry {
name: string;
version: string;
dirname: string;
}
interface IPackageInfo {
name: string;
version: string;
dirname: string; // could be different than name
dependencies: string[];
}
export async function resolveNativeModules(workingdir: string, packages?: string[]): Promise<IUnimoduleEntry[]> {
const resolver = new Resolver(workingdir);
await resolver.init();
if (!packages) {
resolver.addAll();
} else {
const corePackages = await readCorePackages(workingdir);
for (const pkg of corePackages) {
if (resolver.isUnimodule(pkg)) {
resolver.addModule(pkg);
}
}
for (const pkg of packages) {
if (resolver.isUnimodule(pkg)) {
resolver.addModule(pkg);
}
}
}
return resolver.getModules();
}
async function readCorePackages(workingdir: string) {
const pkgJsonPath = path.join(workingdir, 'node_modules', 'react-native-unimodules', 'package.json');
const expoPkgJsonPath = path.join(workingdir, 'packages', 'expo', 'package.json');
if (await fs.pathExists(pkgJsonPath) && await fs.pathExists(expoPkgJsonPath)) {
const packageJson = JSON.parse(await fs.readFile(pkgJsonPath, 'utf-8'));
const expoPackageJson = JSON.parse(await fs.readFile(expoPkgJsonPath, 'utf-8'));
return keys(packageJson.dependencies).concat(keys(expoPackageJson.dependencies));
} else {
logger.warn('No core packages detected');
return [];
}
}
interface IDependencyInfo {
[key: string]: IPackageInfo;
}
class Resolver {
private workingdir: string;
private modulesMap: {[key: string]: IUnimoduleEntry};
private dependencyMap: IDependencyInfo;
constructor(workingdir: string) {
this.workingdir = workingdir;
this.modulesMap = {};
this.dependencyMap = {};
}
public async init() {
this.dependencyMap = await generateDependenciesInfo(this.workingdir);
}
public isUnimodule(moduleName: string): boolean {
return !!this.dependencyMap[moduleName];
}
public addAll() {
for (const moduleName of Object.keys(this.dependencyMap)) {
this.addModule(moduleName);
}
}
public addModule(moduleName: string) {
if (this.dependencyMap[moduleName]) {
this.resolveModule(moduleName);
} else {
throw new Error('Attempting to add unknown module');
}
}
public getModules(): IUnimoduleEntry[] {
return values(this.modulesMap);
}
private async resolveModule(moduleName: string) {
if (this.modulesMap[moduleName]) {
return;
}
const pkg = this.dependencyMap[moduleName];
logger.info(`Adding ${moduleName}:${pkg.version}`);
this.modulesMap[moduleName] = {
name: pkg.name,
dirname: pkg.dirname,
version: pkg.version,
};
for (const dep of pkg.dependencies) {
this.resolveModule(dep);
}
}
}
async function generateDependenciesInfo(workingdir: string): Promise<IDependencyInfo> {
const dir = await fs.readdir(path.join(workingdir, 'packages'));
const map: IDependencyInfo = {};
// scrap directory for package metadata
for (const pkgName of dir) {
const pkg = await getPackage(workingdir, pkgName);
if (pkg) {
map[pkg.name] = pkg;
}
}
// handle @unimodules directory
const unimodulesDir = await fs.readdir(path.join(workingdir, 'packages', '@unimodules'));
for (const pkgName of unimodulesDir) {
const pkg = await getPackage(workingdir, path.join('@unimodules', pkgName));
if (pkg) {
map[pkg.name] = pkg;
}
}
// remove non unimodule dependencies
for (const pkg of Object.keys(map)) {
map[pkg].dependencies = filter(map[pkg].dependencies, (dep) => !!map[dep]);
}
return map;
}
async function getPackage(workingdir: string, pkgName: string): Promise<IPackageInfo | null> {
const pkgPath = path.join(workingdir, 'packages', pkgName);
const isAndroid = await fs.pathExists(path.join(pkgPath, 'android'));
const isIos = await fs.pathExists(path.join(pkgPath, 'ios'));
const isUnimodule = await fs.pathExists(path.join(pkgPath, 'unimodule.json'));
const hasPackageJson = await fs.pathExists(path.join(pkgPath, 'package.json'));
if (!isUnimodule || !hasPackageJson || (!isAndroid && !isIos)) {
return null;
}
const packageJson = JSON.parse(await fs.readFile(path.join(pkgPath, 'package.json'), 'utf-8'));
const pkgDeps = merge({}, // reading everything to make sure
packageJson.dependencies,
packageJson.peerDependencies,
packageJson.unimodulePeerDependencies,
);
const info: IPackageInfo = {
name: packageJson.name,
version: packageJson.version,
dirname: pkgName,
dependencies: Object.keys(pkgDeps),
};
return info;
}
+3 -3
View File
@@ -2,24 +2,24 @@ import * as fs from 'fs-extra';
import { uploadFile } from 'turtle/aws/s3';
import config from 'turtle/config';
import logger from 'turtle/logger';
interface IUploadCtx {
fakeUploadBuildPath?: string;
s3FileKey?: string;
uploadPath: string;
logger: any;
}
export async function uploadBuildToS3(ctx: IUploadCtx) {
if (config.builder.fakeUpload) {
const l = logger.child({ buildPhase: 'copying build artifact' });
const l = ctx.logger.child({ buildPhase: 'copying build artifact' });
const { fakeUploadBuildPath, uploadPath } = ctx;
l.info('copying build to fake upload directory');
await fs.copy(uploadPath, fakeUploadBuildPath as string);
l.info(`copied build to ${fakeUploadBuildPath}`);
return fakeUploadBuildPath as string;
} else {
const l = logger.child({ buildPhase: 'uploading to S3' });
const l = ctx.logger.child({ buildPhase: 'uploading to S3' });
l.info('uploading build artifact to S3');
const { Location: fileLocation } = await uploadFile({
key: ctx.s3FileKey as string,
+12 -18
View File
@@ -1,36 +1,30 @@
import { ExponentTools } from '@expo/xdl';
import fs from 'fs-extra';
import path from 'path';
import { ExponentTools } from 'xdl';
import config from 'turtle/config';
import { IJob } from 'turtle/job';
interface IVersionsMap {
[platform: string]: number[];
}
const versions: IVersionsMap = {};
let SUPPORTED_SDK_VERSIONS: number[];
export async function ensureCanBuildSdkVersion(job: IJob) {
if (config.builder.useLocalWorkingDir) {
return;
}
const platformVersions = await findSupportedSdkVersions(job.platform);
if (!SUPPORTED_SDK_VERSIONS) {
SUPPORTED_SDK_VERSIONS = await findSupportedSdkVersions(job.platform);
}
const targetMajorSdkVersion = ExponentTools.parseSdkMajorVersion(job.sdkVersion || job.manifest.sdkVersion);
if (!platformVersions.includes(targetMajorSdkVersion)) {
if (!SUPPORTED_SDK_VERSIONS.includes(targetMajorSdkVersion)) {
throw new Error(`Unsupported SDK Version!`);
}
}
export async function findSupportedSdkVersions(platform: 'android' | 'ios'): Promise<number[]> {
if (versions[platform]) {
return versions[platform];
} else {
const SDK_DIR_PREFIX = 'sdk';
const files = await fs.readdir(path.join(config.directories.workingDir, platform));
const sdks = files.filter((file) => file.startsWith(SDK_DIR_PREFIX));
versions[platform] = sdks.map((sdk) => parseInt(sdk.substr(SDK_DIR_PREFIX.length), 10));
return versions[platform];
}
async function findSupportedSdkVersions(platform: 'android' | 'ios'): Promise<number[]> {
const SDK_DIR_PREFIX = 'sdk';
const files = await fs.readdir(path.join(config.directories.workingDir, platform));
const sdks = files.filter((file) => file.startsWith(SDK_DIR_PREFIX));
return sdks.map((sdk) => parseInt(sdk.substr(SDK_DIR_PREFIX.length), 10));
}
+2 -2
View File
@@ -1,6 +1,6 @@
import { ANDROID_BUILD_TYPES, IOS_BUILD_TYPES } from 'turtle/constants';
import { IOS } from 'turtle/constants';
export interface IShellAppDirectoryConfig {
sdkVersion: string;
buildType?: IOS_BUILD_TYPES & ANDROID_BUILD_TYPES;
buildType?: IOS.BUILD_TYPES;
}
+6 -9
View File
@@ -2,7 +2,7 @@ import os from 'os';
import path from 'path';
import { PLATFORMS } from 'turtle/constants';
import { env, envBoolean, envNum, envOptional, envTransform } from 'turtle/utils/env';
import { env, envNum, envOptional, envTransform } from 'turtle/utils/env';
export default {
env: env('NODE_ENV'),
@@ -37,19 +37,16 @@ export default {
intervalMs: envNum('AWS_CLOUDWATCH_INTERVAL_MS', 30000),
namespace: env('AWS_CLOUDWATCH_NAMESPACE', 'Turtle'),
},
datadog: {
disabled: envBoolean('DATADOG_DISABLED', false),
apiKey: envOptional('DATADOG_API_KEY'),
appKey: envOptional('DATADOG_APP_KEY'),
intervalMs: env('DATADOG_INTERVAL_MS', 5 * 60 * 1000),
},
redis: {
url: env('REDIS_URL'),
configUrl: env('REDIS_CONFIG_URL'),
},
logger: {
level: env('LOGGER_LEVEL', 'info'),
intervalMs: envNum('LOGGER_INTERVAL_MS', 5000),
interval: envNum('LOGGER_INTERVAL', 5),
client: {
level: env('CLIENT_LOGGER_LEVEL', 'info'),
},
},
google: {
credentials: env('GOOGLE_APPLICATION_CREDENTIALS', ''),
@@ -70,7 +67,7 @@ export default {
(val: string) => env('NODE_ENV') === 'development' && val === '1',
),
fakeUpload: envTransform('TURTLE_FAKE_UPLOAD', '0', (val: string) => val === '1'),
maxJobTimeMs: envNum('TURTLE_MAX_JOB_TIME_MS', 15 * 60 * 1000),
maxJobTimeMs: envNum('TURTLE_MAX_JOB_TIME_MS', 60 * 60 * 1000),
useLocalWorkingDir: envTransform('TURTLE_USE_LOCAL_WORKING_DIR', '0', (val: string) => val === '1'),
},
directories: {
+2 -17
View File
@@ -1,24 +1,9 @@
import * as BUILD from './build';
import * as IOS from './ios';
export enum PLATFORMS {
IOS = 'ios',
ANDROID = 'android',
}
export enum IOS_BUILD_TYPES {
ARCHIVE = 'archive',
CLIENT = 'client',
SIMULATOR = 'simulator',
}
export enum ANDROID_BUILD_TYPES {
APK = 'apk',
APP_BUNDLE = 'app-bundle',
}
export enum ANDROID_BUILD_MODES {
DEBUG = 'debug',
RELEASE = 'release',
}
export { BUILD };
export { IOS, BUILD };
+5
View File
@@ -0,0 +1,5 @@
export enum BUILD_TYPES {
ARCHIVE = 'archive',
CLIENT = 'client',
SIMULATOR = 'simulator',
}
+2 -6
View File
@@ -1,4 +1,4 @@
import { ANDROID_BUILD_MODES, ANDROID_BUILD_TYPES, IOS_BUILD_TYPES } from 'turtle/constants';
import { IOS } from 'turtle/constants';
export interface IJob {
platform: 'android' | 'ios';
@@ -9,12 +9,9 @@ export interface IJob {
config: {
releaseChannel: string;
publicUrl?: string;
buildType: IOS_BUILD_TYPES & ANDROID_BUILD_TYPES;
// ios
buildType?: IOS.BUILD_TYPES;
bundleIdentifier?: string;
// android
androidPackage?: string;
buildMode?: ANDROID_BUILD_MODES;
};
credentials: {
// android
@@ -26,7 +23,6 @@ export interface IJob {
certP12?: string;
certPassword?: string;
provisioningProfile?: string;
provisioningProfileId?: string;
teamId?: string;
appleSession?: string;
udids?: string[];
+36 -40
View File
@@ -6,7 +6,6 @@ import BuildError from 'turtle/builders/BuildError';
import { logErrorOnce } from 'turtle/builders/utils/common';
import config from 'turtle/config';
import { BUILD } from 'turtle/constants/index';
import logger from 'turtle/logger';
import * as buildDurationMetric from 'turtle/metrics/buildDuration';
import * as buildStatusMetric from 'turtle/metrics/buildStatus';
import { checkShouldExit, setCurrentJobId, turtleVersion } from 'turtle/turtleContext';
@@ -14,35 +13,35 @@ import { getPriorities } from 'turtle/utils/priorities';
import * as redis from 'turtle/utils/redis';
import { sanitizeJob } from 'turtle/validator';
function _maybeExit() {
function _maybeExit(logger: any) {
if (checkShouldExit()) {
logger.warn('Exiting due to previously received termination signal.');
process.exit();
}
}
export async function doJob() {
const jobData = await getJob();
await processJob(jobData);
_maybeExit();
export async function doJob(logger: any) {
const jobData = await getJob(logger);
await processJob(jobData, logger);
_maybeExit(logger);
}
export async function getJob() {
_maybeExit();
export async function getJob(logger: any) {
_maybeExit(logger);
logger.info('Fetching job');
while (true) {
try {
let job = null;
const priorities = await getPriorities();
const priorities = await getPriorities(logger);
for (const priority of priorities) {
job = await sqs.receiveMessage(priority);
_maybeExit();
job = await sqs.receiveMessage(priority, logger);
_maybeExit(logger);
if (job != null) {
job.priority = priority;
break;
}
}
_maybeExit();
_maybeExit(logger);
if (job !== null) {
return job;
}
@@ -52,37 +51,34 @@ export async function getJob() {
}
}
async function processJob(jobData: any) {
async function processJob(jobData: any, logger: any) {
const receiptHandle = jobData.ReceiptHandle;
const { priority } = jobData;
let timeoutId;
try {
const rawJob = JSON.parse(jobData.Body);
timeoutId = failAfterMaxJobTime(priority, receiptHandle, rawJob);
timeoutId = failAfterMaxJobTime(priority, receiptHandle, rawJob, logger);
const job = await sanitizeJob(rawJob);
setCurrentJobId(job.id);
const pingerHandle = sqs.changeMessageVisibilityRecurring(priority, jobData.ReceiptHandle, job.id);
const pingerHandle = sqs.changeMessageVisibilityRecurring(priority, jobData.ReceiptHandle, job.id, logger);
logger.info(
`Doing job MessageId=${jobData.MessageId} BuildId=${job.id} `
+ `Priority=${priority.slice(0, priority.length - 8)} ${Date.now()}`,
);
logger.info(`Doing job MessageId=${jobData.MessageId} BuildId=${job.id} ${Date.now()}`);
const cancelled = await redis.checkIfCancelled(job.id);
const cancelled = await redis.checkIfCancelled(job.id, logger);
if (cancelled) {
logger.info('The job has been cancelled');
} else {
redis.registerListener(job.id, () => deleteMessage(priority, receiptHandle));
redis.registerListener(job.id, () => deleteMessage(priority, receiptHandle, logger), logger);
const startTimestamp = Date.now();
let buildFailed = false;
const buildType = _.get(job, 'config.buildType', 'default');
try {
const status = await build(job);
buildStatusMetric.add(buildType, priority, status);
const status = await build(job, logger);
buildStatusMetric.add(buildType, status);
buildFailed = !status;
} catch (err) {
buildStatusMetric.add(buildType, priority, false);
buildStatusMetric.add(buildType, false);
buildFailed = true;
throw err;
} finally {
@@ -96,13 +92,13 @@ async function processJob(jobData: any) {
);
buildDurationMetric.addTotalDuration(buildType, totalBuildDurationSecs, !buildFailed);
}
redis.unregisterListeners();
redis.unregisterListeners(logger);
}
logger.info(`Job done MessageId=${jobData.MessageId} BuildId=${job.id} ${Date.now()}`);
}
clearInterval(pingerHandle);
} finally {
await deleteMessage(priority, receiptHandle);
await deleteMessage(priority, receiptHandle, logger);
if (timeoutId) {
logger.info('Clearing job failer timeout...');
clearTimeout(timeoutId);
@@ -110,39 +106,39 @@ async function processJob(jobData: any) {
}
}
function failAfterMaxJobTime(priority: string, receiptHandle: string, job: any) {
function failAfterMaxJobTime(priority: string, receiptHandle: string, job: any, logger: any) {
return setTimeout(async () => {
try {
sqs.sendMessage(job.id, BUILD.JOB_STATES.ERRORED, { turtleVersion });
await deleteMessage(priority, receiptHandle);
sqs.sendMessage(job.id, BUILD.JOB_STATES.ERRORED, { turtleVersion }, logger);
await deleteMessage(priority, receiptHandle, logger);
} finally {
logger.error('Build timed out. Going to terminate turtle agent.');
await logger.cleanup();
logger.error('Going to terminate turtle agent, just in case...');
process.exit(1);
}
}, config.builder.maxJobTimeMs);
}
async function build(job: any) {
async function build(job: any, logger: any) {
const startTimestamp = Date.now();
const s3Url = await logger.initForJob(job);
const s3Url = await logger.init(job);
const buildLogger = logger.child({ jobID: job.id, experienceName: job.experienceName });
const calculateBuildDuration = () => Math.ceil((Date.now() - startTimestamp) / 1000);
try {
await sqs.sendMessage(job.id, BUILD.JOB_STATES.IN_PROGRESS, {
logUrl: s3Url,
logFormat: 'json',
});
const result = await (builders as any)[job.platform](job);
}, buildLogger);
const result = await builders[job.platform](job, buildLogger);
const buildDuration = calculateBuildDuration();
sqs.sendMessage(job.id, BUILD.JOB_STATES.FINISHED, {
...result,
turtleVersion,
buildDuration,
});
}, buildLogger);
return true;
} catch (err) {
logErrorOnce(err);
logErrorOnce(err, buildLogger);
const buildDuration = calculateBuildDuration();
let reason;
if (err instanceof BuildError) {
@@ -152,14 +148,14 @@ async function build(job: any) {
turtleVersion,
buildDuration,
...reason && { reason },
});
}, buildLogger);
return false;
} finally {
await logger.cleanup();
await logger.cleanup(job);
}
}
async function deleteMessage(priority: string, receiptHandle: string) {
async function deleteMessage(priority: string, receiptHandle: string, logger: any) {
try {
setCurrentJobId(null);
await sqs.deleteMessage(priority, receiptHandle);
+1 -5
View File
@@ -1,6 +1,6 @@
import Joi from 'joi';
import { ANDROID_BUILD_MODES, ANDROID_BUILD_TYPES, PLATFORMS } from 'turtle/constants';
import { PLATFORMS } from 'turtle/constants';
import baseJobSchema from 'turtle/jobsSchemas/base';
export default baseJobSchema.concat(
@@ -12,9 +12,5 @@ export default baseJobSchema.concat(
keystoreAlias: Joi.string(),
keystore: Joi.string(),
}),
config: Joi.object().keys({
buildMode: Joi.string().valid(Object.values(ANDROID_BUILD_MODES)).default(ANDROID_BUILD_MODES.RELEASE),
buildType: Joi.string().valid(Object.values(ANDROID_BUILD_TYPES)).default(ANDROID_BUILD_TYPES.APK),
}),
}),
);
+3 -4
View File
@@ -1,13 +1,13 @@
import Joi from 'joi';
import { IOS_BUILD_TYPES, PLATFORMS } from 'turtle/constants/index';
import { IOS, PLATFORMS } from 'turtle/constants/index';
import baseJobSchema from 'turtle/jobsSchemas/base';
export default baseJobSchema.concat(
Joi.object().keys({
platform: Joi.string().valid(PLATFORMS.IOS),
config: Joi.object().keys({
buildType: Joi.string().default(IOS_BUILD_TYPES.ARCHIVE),
buildType: Joi.string().default(IOS.BUILD_TYPES.ARCHIVE),
bundleIdentifier: Joi.string().regex(/^[a-zA-Z][a-zA-Z0-9\-.]+$/),
}),
credentials: Joi.object()
@@ -17,12 +17,11 @@ export default baseJobSchema.concat(
teamId: Joi.string().required(),
// it's not required for adhoc Expo Client builds
provisioningProfile: Joi.string(),
provisioningProfileId: Joi.string(),
appleSession: Joi.string(),
udids: Joi.array().items(Joi.string()),
})
.when('config.buildType', {
is: IOS_BUILD_TYPES.SIMULATOR,
is: IOS.BUILD_TYPES.SIMULATOR,
then: Joi.optional(),
otherwise: Joi.required(),
}),
+71 -65
View File
@@ -1,76 +1,82 @@
import bunyan from 'bunyan';
import bunyanDebugStream from 'bunyan-debug-stream';
import { SentryStream } from 'bunyan-sentry-stream';
import { Client as RavenClient } from 'raven';
import config from 'turtle/config';
import { IJob } from 'turtle/job';
import streams from 'turtle/logger/streams';
import S3Stream from 'turtle/logger/s3Stream';
import { isOffline } from 'turtle/turtleContext';
interface ILoggerFields {
[key: string]: string | number;
// type error when using import
// https://github.com/googleapis/nodejs-logging-bunyan/issues/241
// tslint:disable-next-line:no-var-requires
const { LoggingBunyan } = require('@google-cloud/logging-bunyan');
interface IStream {
stream: any;
type?: string;
level?: string;
reemitErrorEvents?: boolean;
}
class Logger {
private parentLogger: any;
private currentLogger: any;
export const s3logger = new S3Stream();
constructor(parentLogger?: any) {
this.parentLogger = parentLogger || bunyan.createLogger({
name: 'turtle',
level: config.logger.level,
platform: config.platform,
serializers: bunyan.stdSerializers,
...config.deploymentEnv && { environment: config.deploymentEnv },
streams: Object.values(streams),
});
this.currentLogger = this.parentLogger;
}
const streams: IStream[] = [];
public async initForJob(job: IJob) {
this.currentLogger = this.parentLogger.child({
jobID: job.id,
experienceName: job.experienceName,
});
const s3Url = await streams.s3.stream.init(job);
return s3Url;
}
public async cleanup() {
this.currentLogger = this.parentLogger;
await streams.s3.stream.cleanup();
}
public trace(...all: any[]) {
this.currentLogger.trace(...all);
}
public debug(...all: any[]) {
this.currentLogger.debug(...all);
}
public info(...all: any[]) {
this.currentLogger.info(...all);
}
public warn(...all: any[]) {
this.currentLogger.warn(...all);
}
public error(...all: any[]) {
this.currentLogger.error(...all);
}
public fatal(...all: any[]) {
this.currentLogger.fatal(...all);
}
public child(fields: ILoggerFields) {
const newLogger = this.currentLogger.child(fields);
return new Logger(newLogger);
}
// only for backward compatibility
public withFields(fields: ILoggerFields) {
return this.child(fields);
}
if (isOffline()) {
const prettyStdOut = new bunyanDebugStream({ forceColor: true });
streams.push({ stream: prettyStdOut, type: 'raw', level: config.logger.client.level });
} else {
streams.push(
{ stream: process.stdout },
{
type: 'raw',
stream: s3logger,
reemitErrorEvents: true,
level: config.logger.client.level,
},
);
}
export default new Logger();
if (config.sentry.dsn && config.deploymentEnv !== 'development') {
const raven = new RavenClient();
raven.config(config.sentry.dsn, { environment: config.deploymentEnv });
streams.push({
level: 'error',
type: 'raw', // Mandatory type for SentryStream
stream: new SentryStream(raven),
});
}
if (config.google.credentials) {
const resource = {
type: 'generic_node',
labels: {
node_id: config.hostname,
location: '', // default value
namespace: '', // default value
},
};
streams.push(new LoggingBunyan({ name: 'turtle', resource }).stream('info'));
}
const logger = bunyan.createLogger({
name: 'turtle',
level: config.logger.level,
platform: config.platform,
environment: config.deploymentEnv,
streams,
});
logger.init = async (job: IJob) => {
return await s3logger.init(job);
};
logger.cleanup = async () => {
// a little hacky, but works
logger.info({ lastBuildLog: true }, 'this is the last log from this build');
await s3logger.waitForLogger();
};
export default logger;
+111
View File
@@ -0,0 +1,111 @@
import EventEmitter from 'events';
import path from 'path';
import fs from 'fs-extra';
import _ from 'lodash';
import { uploadFile } from 'turtle/aws/s3';
import config from 'turtle/config';
import * as constants from 'turtle/constants/logger';
import { IJob } from 'turtle/job';
export default class S3Stream extends EventEmitter {
private s3Url: string | null;
private lastFlush: number | null;
private filePath: string | null;
private fileHandle: number | null;
private finishedPromise: Promise<any> | null;
private finishedPromiseResolveFn: any;
private secrets: any;
constructor() {
super();
this.s3Url = null;
this.lastFlush = null;
this.filePath = null;
this.fileHandle = null;
this.finishedPromise = null;
this.finishedPromiseResolveFn = null;
}
public async init(job: IJob) {
this.s3Url = `logs/${job.experienceName}/${job.id}`;
const dir = config.directories.tempS3LogsDir;
const exists = await fs.pathExists(dir);
if (!exists) {
await fs.mkdir(dir);
}
this.filePath = path.join(dir, job.id);
this.fileHandle = await fs.open(this.filePath, 'w+', 0o660);
this.secrets = {};
this.finishedPromise = new Promise((resolveFn) => (this.finishedPromiseResolveFn = resolveFn));
const res = await this.flush() as any;
return res.Location;
}
public async cleanup() {
const res = this.finishedPromiseResolveFn;
try {
await this.flush(true);
await fs.close(this.fileHandle as number);
this.fileHandle = null;
this.lastFlush = null;
this.secrets = {};
} finally {
this.finishedPromise = null;
this.finishedPromiseResolveFn = null;
res();
}
}
public flush(force = false) {
if (force || !this.lastFlush) {
return this._flush();
}
const now = Date.now();
if (now - this.lastFlush > config.logger.interval) {
return this._flush();
}
}
public write(recRaw: any) {
if (this.fileHandle) {
const rec = JSON.stringify({
...recRaw,
level: constants.LEVELS_MAPPING[recRaw.level].toLowerCase(),
});
if (recRaw.lastBuildLog) {
return this.cleanup();
}
const recSanitized = Object.keys(this.secrets).reduce(
(acc, secret) => acc.replace(new RegExp(secret, 'g'), this.secrets[secret]),
rec,
);
fs
.write(this.fileHandle as number, `${recSanitized}\n`)
.then(() => {
this.flush();
});
}
}
public addSecretsToFilter(secrets: any) {
const filteredSecrets = secrets.filter((i: any) => i !== undefined && i !== null);
const replacements = filteredSecrets.map((i: any) => 'X'.repeat(i.length));
const toAdd = _.zipObject(filteredSecrets, replacements);
this.secrets = Object.assign({}, this.secrets, toAdd);
}
public waitForLogger() {
return this.finishedPromise;
}
private _flush() {
this.lastFlush = Date.now();
return uploadFile({
srcPath: this.filePath as string,
key: this.s3Url as string,
});
}
}
-29
View File
@@ -1,29 +0,0 @@
import config from 'turtle/config';
// type error when using import
// https://github.com/googleapis/nodejs-logging-bunyan/issues/241
// tslint:disable-next-line:no-var-requires
const { LoggingBunyan } = require('@google-cloud/logging-bunyan');
export default function create() {
if (!config.google.credentials) {
return null;
}
const gcloudStream = new LoggingBunyan({
name: 'turtle',
resource: {
type: 'generic_node',
labels: {
node_id: config.hostname,
location: '', // default value
namespace: '', // default value
},
},
});
return {
level: config.logger.level,
type: 'raw',
stream: gcloudStream,
};
}
-38
View File
@@ -1,38 +0,0 @@
import pickBy from 'lodash/pickBy';
import createGCloudStream from 'turtle/logger/streams/gcloud';
import createOfflineStream from 'turtle/logger/streams/offline';
import createS3Stream from 'turtle/logger/streams/s3';
import createSentryStream from 'turtle/logger/streams/sentry';
import { isOffline } from 'turtle/turtleContext';
interface IStream {
stream: any;
type?: string;
level?: string;
reemitErrorEvents?: boolean;
}
interface IStreamMap {
[key: string]: IStream;
}
function prepareStreams(): IStreamMap {
if (isOffline()) {
return {
stdout: createOfflineStream(),
};
} else {
const allStreams = {
stdout: { stream: process.stdout },
gcloud: createGCloudStream(),
s3: createS3Stream(),
sentry: createSentryStream(),
};
return pickBy(allStreams) as IStreamMap;
}
}
const streams = prepareStreams();
export default streams;
-11
View File
@@ -1,11 +0,0 @@
import bunyanDebugStream from 'bunyan-debug-stream';
import config from 'turtle/config';
export default function create() {
const prettyStdOut = new bunyanDebugStream({ forceColor: true });
return {
stream: prettyStdOut,
type: 'raw',
level: config.logger.level,
};
}
-106
View File
@@ -1,106 +0,0 @@
import EventEmitter from 'events';
import path from 'path';
import fs from 'fs-extra';
import _ from 'lodash';
import { uploadFile } from 'turtle/aws/s3';
import config from 'turtle/config';
import { IJob } from 'turtle/job';
export default function create() {
const s3 = new S3Stream();
return {
type: 'raw',
stream: s3,
reemitErrorEvents: true,
level: config.logger.level,
};
}
class S3Stream extends EventEmitter {
private s3Url: string | null = null;
private lastFlush: number | null = null;
private filePath: string | null = null;
private fileHandle: number | null = null;
private uploadingPromise: Promise<void> | null = null;
private uploadingPromiseResolveFn: any;
private waitingOnPromise: boolean = false;
public async init(job: IJob) {
this.s3Url = `logs/${job.experienceName}/${job.id}`;
const dir = config.directories.tempS3LogsDir;
const exists = await fs.pathExists(dir);
if (!exists) {
await fs.mkdir(dir);
}
this.filePath = path.join(dir, job.id);
this.fileHandle = await fs.open(this.filePath, 'w+', 0o660);
this.lastFlush = null;
const res = await this.flush() as any;
return res.Location;
}
public async cleanup() {
await this.flush(true);
await fs.close(this.fileHandle as number);
this.fileHandle = null;
this.waitingOnPromise = false;
}
public flush(force = false) {
if (force || !this.lastFlush) {
return this.flushInternal();
}
const now = Date.now();
if (now - this.lastFlush > config.logger.intervalMs) {
return this.flushInternal();
}
}
public write(rec: any) {
if (!this.fileHandle) {
return;
}
fs
.write(this.fileHandle as number, `${JSON.stringify(rec)}\n`)
.then(() => {
this.flush();
});
}
private flushInternal(): any {
this.lastFlush = Date.now();
if (!this.uploadingPromise) {
this.uploadingPromise = new Promise((res) => {
this.uploadingPromiseResolveFn = res;
});
return this
.upload()
.then((result) => {
setTimeout(() => this.uploadingPromiseResolveFn(), 0);
return result;
});
} else {
if (this.waitingOnPromise) {
return;
}
this.waitingOnPromise = true;
return this.uploadingPromise
.then(() => {
this.waitingOnPromise = false;
this.uploadingPromise = null;
return this.flushInternal();
});
}
}
private upload() {
return uploadFile({
srcPath: this.filePath as string,
key: this.s3Url as string,
});
}
}
-18
View File
@@ -1,18 +0,0 @@
import { SentryStream } from 'bunyan-sentry-stream';
import { Client as RavenClient } from 'raven';
import config from 'turtle/config';
export default function create() {
if (!config.sentry.dsn || config.deploymentEnv === 'development') {
return null;
}
const raven = new RavenClient();
raven.config(config.sentry.dsn, { environment: config.deploymentEnv });
return {
level: 'error',
type: 'raw', // Mandatory type for SentryStream
stream: new SentryStream(raven),
};
}
+1 -2
View File
@@ -1,11 +1,10 @@
import * as cloudwatch from 'turtle/aws/cloudwatch';
import { TYPE_DIMENSIONS } from 'turtle/aws/cloudwatch/dimensions';
import logger from 'turtle/logger';
const TURTLE_BUILD_DURATION_METRIC_NAME = 'turtleBuildDuration';
const TOTAL_BUILD_DURATION_METRIC_NAME = 'totalBuildDuration';
export function register() {
export function register(logger: any) {
for (const metricName of [TURTLE_BUILD_DURATION_METRIC_NAME, TOTAL_BUILD_DURATION_METRIC_NAME]) {
logger.info(`Registered AWS Cloudwatch metric: ${metricName}`);
cloudwatch.registerMetric({
+4 -5
View File
@@ -1,12 +1,11 @@
import * as cloudwatch from 'turtle/aws/cloudwatch';
import { PRIORITY_DIMENSIONS } from 'turtle/aws/cloudwatch/dimensions';
import { TYPE_DIMENSIONS } from 'turtle/aws/cloudwatch/dimensions';
import * as reducers from 'turtle/aws/cloudwatch/reducers';
import config from 'turtle/config';
import logger from 'turtle/logger';
const BUILD_METRIC_NAME = 'build';
export function register() {
export function register(logger: any) {
logger.info(`Registered AWS Cloudwatch metric: ${BUILD_METRIC_NAME}`);
cloudwatch.registerMetric({
metricName: BUILD_METRIC_NAME,
@@ -24,11 +23,11 @@ export function register() {
});
}
export function add(type: string, priority: string, success = true) {
export function add(type: string, success = true) {
cloudwatch.addMetricData({
name: BUILD_METRIC_NAME,
value: 1,
success,
extraDimensions: PRIORITY_DIMENSIONS(type, priority),
extraDimensions: TYPE_DIMENSIONS(type),
});
}
-61
View File
@@ -1,61 +0,0 @@
import groupBy from 'lodash/groupBy';
import mapValues from 'lodash/mapValues';
import take from 'lodash/take';
import logger from 'turtle/logger';
import { sendMetric } from 'turtle/metrics/datadog';
import { getReplicaCount } from 'turtle/utils/metadata';
import { getLabeledConfiguration, TurtleModeLabels } from 'turtle/utils/priorities';
const createMetricName = () => `turtle.replicaCount`;
const createModeMetricName = (mode: TurtleModeLabels) => `${createMetricName()}.${mode}`;
const createModeTag = (mode: TurtleModeLabels) => `mode:${mode}`;
export default async function sendConfigToDatadog() {
let configuration;
try {
configuration = await getLabeledConfiguration();
} catch (err) {
logger.warn('Failed to send the builders configuration to Datadog because it couldn\'t be fetched from Redis');
return;
}
const replicaCount = await getReplicaCount();
const currentReplicaCount = replicaCount === null ? configuration.length : replicaCount;
const replicaCounts = mapValues(
groupBy<string>(take(configuration, currentReplicaCount)),
(i) => i.length,
);
const metricName = createMetricName();
const normalReplicaCount = replicaCounts[TurtleModeLabels.Normal] || 0;
const highReplicaCount = replicaCounts[TurtleModeLabels.High] || 0;
const highOnlyReplicaCount = replicaCounts[TurtleModeLabels.HighOnly] || 0;
const normalTag = createModeTag(TurtleModeLabels.Normal);
const normalMetricName = createModeMetricName(TurtleModeLabels.Normal);
logger.debug(
`Sending ${metricName}=${normalReplicaCount} (${normalTag}), ${normalMetricName}=${normalReplicaCount} to Datadog`,
);
await sendMetric(metricName, normalReplicaCount, [normalTag]);
await sendMetric(normalMetricName, normalReplicaCount);
const highTag = createModeTag(TurtleModeLabels.High);
const highMetricName = createModeMetricName(TurtleModeLabels.High);
logger.debug(
`Sending ${metricName}=${highReplicaCount} (${highTag}), ${highMetricName}=${highReplicaCount} to Datadog`,
);
await sendMetric(metricName, highReplicaCount, [highTag]);
await sendMetric(highMetricName, highReplicaCount);
const highOnlyTag = createModeTag(TurtleModeLabels.HighOnly);
const highOnlyMetricName = createModeMetricName(TurtleModeLabels.HighOnly);
logger.debug(
`Sending ${metricName}=${highOnlyReplicaCount} (${highOnlyTag}),`
+ ` ${highOnlyMetricName}=${highOnlyReplicaCount} to Datadog`,
);
await sendMetric(metricName, highOnlyReplicaCount, [highOnlyTag]);
await sendMetric(highOnlyMetricName, highOnlyReplicaCount);
}
-24
View File
@@ -1,24 +0,0 @@
import dogapi from 'dogapi';
import util from 'util';
import config from 'turtle/config';
const datadogSendMetric = util.promisify(dogapi.metric.send);
if (!config.datadog.disabled) {
dogapi.initialize({
api_key: config.datadog.apiKey,
app_key: config.datadog.appKey,
});
}
const tags = [
`env:${config.deploymentEnv}`,
`platform:${config.platform}`,
];
export function sendMetric(name: string, value: number, additionalTags?: string[]) {
if (!config.datadog.disabled) {
return datadogSendMetric(name, value, { tags: [...tags, ...(additionalTags || [])] });
}
}
+3 -4
View File
@@ -1,12 +1,11 @@
import * as cloudwatch from 'turtle/aws/cloudwatch';
import logger from 'turtle/logger';
import * as buildDuration from 'turtle/metrics/buildDuration';
import * as buildStatus from 'turtle/metrics/buildStatus';
const metricsToRegister = [buildStatus, buildDuration];
export async function init() {
export async function init(logger: any) {
logger.info('Initializing Cloudwatch metrics');
await cloudwatch.init();
metricsToRegister.forEach((metric) => metric.register());
await cloudwatch.init(logger);
metricsToRegister.forEach((metric) => metric.register(logger));
}
+8 -17
View File
@@ -1,15 +1,13 @@
import { LoggerDetach } from '@expo/xdl';
import { LoggerDetach } from 'xdl';
import { findSupportedSdkVersions } from 'turtle/builders/utils/version';
import config from 'turtle/config';
import { doJob } from 'turtle/jobManager';
import logger from 'turtle/logger';
import setup from 'turtle/setup';
import { checkShouldExit, setShouldExit, turtleVersion } from 'turtle/turtleContext';
import { getRedisClient } from 'turtle/utils/redis';
import { getRedisClient, RedisClient } from 'turtle/utils/redis';
const REDIS_TURTLE_VERSION_KEY = 'turtle:version';
const REDIS_TURTLE_SUPPORTED_SDK_VERSIONS_KEY = `turtle:${config.platform}:sdkVersions`;
process.on('unhandledRejection', (err) => logger.error({ err }, 'Unhandled promise rejection'));
@@ -33,27 +31,20 @@ async function main() {
);
LoggerDetach.configure(logger);
if (setup[config.platform]) {
await setup[config.platform]();
await setup[config.platform](logger);
}
try {
const redis = await getRedisClient();
if (redis) {
await redis.set(REDIS_TURTLE_VERSION_KEY, turtleVersion);
logger.info(`Registered Turtle version (${turtleVersion}) in Redis`);
const sdkVersions = (await findSupportedSdkVersions(config.platform)).join(',');
await redis.set(REDIS_TURTLE_SUPPORTED_SDK_VERSIONS_KEY, sdkVersions);
logger.info(`Registered supported SDK versions (${sdkVersions}) in Redis`);
} else {
logger.warn('Couldn\'t set supported SDK versions in Redis (failed to connect to Redis)');
}
const redis = await getRedisClient(logger, RedisClient.Configuration);
await redis.set(REDIS_TURTLE_VERSION_KEY, turtleVersion);
logger.info(`Register Turtle version ${turtleVersion} in Redis`);
} catch (err) {
logger.error({ err }, 'Failed to set versions in Redis');
logger.error({ err }, 'Failed to register Turtle version in Redis');
}
while (true) {
try {
await doJob();
await doJob(logger);
} catch (err) {
logger.error({ err }, 'Failed to do a job');
}
+2 -3
View File
@@ -1,8 +1,7 @@
import logger from 'turtle/logger';
import commonSetup from 'turtle/setup/common';
export default async function setup() {
export default async function setup(logger: any) {
logger.info('Setting up environment...');
await commonSetup();
await commonSetup(logger);
logger.info('Finished setting up environment');
}
+2 -15
View File
@@ -1,23 +1,10 @@
import fs from 'fs-extra';
import config from 'turtle/config';
import logger from 'turtle/logger';
import * as metrics from 'turtle/metrics';
import sendConfigToDatadog from 'turtle/metrics/buildersConfig';
export default async function commonSetup() {
announceBuildersConfig();
await metrics.init();
export default async function commonSetup(logger: any) {
await metrics.init(logger);
await fs.remove(config.directories.temporaryFilesRoot);
await fs.remove(config.directories.tempS3LogsDir);
}
async function announceBuildersConfig() {
try {
await sendConfigToDatadog();
} catch (err) {
logger.error({ err }, 'Failed to send metrics to Datadog');
} finally {
setTimeout(announceBuildersConfig, config.datadog.intervalMs);
}
}
+2 -3
View File
@@ -3,7 +3,6 @@ import os from 'os';
import path from 'path';
import * as keychain from 'turtle/builders/utils/ios/keychain';
import logger from 'turtle/logger';
import commonSetup from 'turtle/setup/common';
async function deleteProvisioningProfilesFromHomedir() {
@@ -17,10 +16,10 @@ async function deleteProvisioningProfilesFromHomedir() {
}
}
export default async function setup() {
export default async function setup(logger: any) {
logger.info('Setting up environment...');
await deleteProvisioningProfilesFromHomedir();
await keychain.cleanUp();
await commonSetup();
await commonSetup(logger);
logger.info('Finished setting up environment');
}
+4 -5
View File
@@ -1,10 +1,9 @@
declare module 'async-retry';
declare module 'copy';
declare module 'xdl';
declare module 'bunyan';
declare module 'bunyan-sentry-stream';
declare module 'bunyan-debug-stream';
declare module 'copy';
declare module 'dogapi';
declare module 'async-retry';
declare module 'ioredis';
declare module 'is-semver';
declare module 'request-progress';
declare module '@expo/xdl';
declare module 'is-semver';
-11
View File
@@ -16,16 +16,5 @@ export const env = (varName: string, defaultValue?: any) => {
export const envOptional = (varName: string) => env(varName, null);
export const envTransform = (varName: string, defaultValue: any, fn: any) => fn(env(varName, defaultValue));
export const envNum = (varName: string, defaultValue?: number) => parseInt(env(varName, defaultValue), 10);
export const envBoolean = (varName: string, defaultValue?: boolean) => {
let defaultString;
if (defaultValue === true) {
defaultString = 'true';
} else if (defaultValue === false) {
defaultString = 'false';
} else {
defaultString = defaultValue;
}
return env(varName, defaultString) === 'true';
};
const isOffline = () => process.env.TURTLE_MODE === 'offline';
Loaded 100 of 116 files, more files were not shown because too many files have changed in this diff. Show more