Compare commits

..
Author SHA1 Message Date
Wojciech Kozyra db74843ae0 Release 0.8.6 2019-06-25 17:05:13 +02:00
Wojciech KozyraandDominik Sokal a0e1382531 update xdl (fix support for binary plist) (#101)
* update xdl (fix support for binary plist)

* Update CHANGELOG.md

* Apply suggestions from code review

Co-Authored-By: Dominik Sokal <[email protected]>
2019-06-25 16:33:15 +02:00
Wojciech Kozyra 952dcd0cd3 fix schema for android buildModes(ts error) 2019-06-19 17:16:35 +02:00
Wojciech Kozyra 0cd4e2953a fix schema for android buildModes 2019-06-19 16:42:48 +02:00
Wojciech Kozyra a03d93cb27 Revert "Release 0.8.5"
This reverts commit 75316b5f07.
2019-06-19 16:42:01 +02:00
Wojciech Kozyra 010e92472b Update CHANGELOG.md 2019-06-19 16:11:18 +02:00
Wojciech Kozyra 75316b5f07 Release 0.8.5 2019-06-19 16:07:40 +02:00
Stanisław Chmiela 4baa49e8fe [android] Support buildMode = debug | release (#99)
* [android] Support buildMode = debug | release

* Upgrade XDL to include buildMode feature
2019-06-19 16:05:17 +02:00
Nick Novitski edb167e6b1 xdl: 54.1.4 -> 55.0.0 (#98) 2019-06-18 10:03:49 +02:00
Stanisław Chmiela d552b97654 [android] Update SDK33 tarball to update Lottie dependency (#97)
Fixes https://github.com/expo/expo/issues/4510 on Turtle
2019-06-17 17:50:46 +02:00
Wojciech Kozyra 12718c0b36 redirect logs to superivsord (#96) 2019-06-17 13:52:03 +02:00
Dominik Sokal f9600f26e8 fix error serialization 2019-06-17 13:36:34 +02:00
Dominik Sokal 68f9744f7b update CHANGELOG 2019-06-14 15:09:23 +02:00
Dominik Sokal 8c2031c3d7 [turtle-cli] don't require gulp-cli installed globally 2019-06-14 15:09:23 +02:00
Dominik Sokal 3165b5346e [turtle-cli] fix updating shell apps (#95) 2019-06-14 10:53:44 +02:00
Dominik Sokal 1552c9e7f2 Release 0.8.4 2019-06-13 16:15:50 +02:00
Dominik Sokal 51a0e772c0 upgraded sharp to 0.22.1 2019-06-13 15:49:35 +02:00
Wojciech Kozyra 98747c1db6 fix eventlistener in supervisord 2019-06-13 13:16:38 +02:00
Dominik Sokal 4f64aa2617 log build priority 2019-06-13 11:33:42 +02:00
Wojciech Kozyra 1f487d39fb update CHANGELOG.md 2019-06-13 11:23:56 +02:00
Wojciech Kozyra f7e647c1f4 Release 0.8.3 2019-06-13 11:22:34 +02:00
Dominik Sokal d900e3c491 unify installing android sdk (in docker and with turtle-cli) (#94) 2019-06-13 11:19:41 +02:00
Wojciech KozyraandDominik Sokal 32b049df86 add supervisord (#93)
* add supervisord

- restart pod only if there is at least 5 failures in 10 minutes

* Update CHANGELOG.md

Co-Authored-By: Dominik Sokal <[email protected]>

* Update scripts/android/supervisordEventListener.sh

Co-Authored-By: Dominik Sokal <[email protected]>
2019-06-13 10:53:30 +02:00
Wojciech Kozyra 723c84b575 update CHANGELOG 2019-06-13 10:47:49 +02:00
Wojciech KozyraandDominik Sokal 0ab4d2ed90 fetch manifest in turtle-cli (#91)
* fetch manifest in turtle-cli

* Update CHANGELOG.md

Co-Authored-By: Dominik Sokal <[email protected]>
2019-06-12 13:21:53 +02:00
Dominik Sokal 05b96a5b6f update builders configuration to the current state 2019-06-12 09:55:44 +02:00
Dominik Sokal 5467aca0fb update CHANGELOG 2019-06-11 17:55:17 +02:00
Dominik Sokal cf1e1daa26 Release 0.8.2 2019-06-11 17:54:15 +02:00
Dominik Sokal 8649968531 turtle-cli fixes for sdk 33 (#90)
* fix setting up android shell app (sdk 33)

* don't fail turtle-cli if manifest is falsy

* updated CHANGELOG
2019-06-11 17:53:32 +02:00
Stanisław Chmiela b89214fcc9 [android] Remove face detector and stripe from Android standalone builds (#84)
* [android] Update SDK33 tarball to remove face detector and payments stripe from enabled modules

* [changelog] Update CHANGELOG
2019-06-11 11:00:10 +02:00
Dominik Sokal 0142147a61 update CHANGELOG 2019-06-10 16:07:51 +02:00
Dominik Sokal 331c8edba7 Release 0.8.1 2019-06-10 15:43:09 +02:00
Wojciech Kozyra ed382e0d2c Update CHANGELOG.md 2019-06-10 13:57:43 +02:00
Wojciech Kozyra 3f0958845d read dependencies from expo package when resolving unimodules (#83)
* read dependencies from expo package when resolving unimodules

* Update CHANGELOG.md
2019-06-10 13:52:29 +02:00
Wojciech Kozyra 6e345757e3 Update CHANGELOG.md 2019-06-10 13:49:00 +02:00
Dominik Sokal 3588c5a5c4 don't move workingdir to /tmp 2019-06-10 12:57:03 +02:00
Stanisław Chmiela 1f94e38e60 Add support for universe-package.json-less shell apps everywhere (#82)
* Fix building SDK33 Android tarball without universe-package.json in single build mode

* Add CHANGELOG entry
2019-06-10 09:42:19 +02:00
Wojciech Kozyra ca8465d2a1 Release 0.8.0 2019-06-07 15:31:42 +02:00
Wojciech Kozyra fe31b74471 support android app bundles (#80)
* support android app bundles

* add android build types in turtle-cli

* bump xdl version to 54.1.4

* update CHANGELOG.md
2019-06-07 15:30:20 +02:00
Tomasz Sapeta ed12f3c34b SDK33 (#75)
* [android] Add SDK33 shell tarball

* install rsync in docker

* drop support for sdk 26

* don't yarn install for sdk 33

* make build logs cleaner

* hopefully fix Dockerfile (can't test this locally)

* really fix dockerfile

* revert yarn install for sdk 33

* move setting NODE_ENV up

* [ios] add SDK33 shell tarball

* [ios] add adhoc shell tarball

* mv workingdir to /tmp prior to running yarn install

* [android] Update shell tarball for SDK33 with [email protected]

* Update SDK33 Android tarball with Android 8.0 fixes, AppAuth fixes and updated JSC

* [android] Update shell tarball for SDK32 with proper underlying native RN libraries and JSC (#78)

* Upgrade XDL dependency to 54.1.3 to include https://github.com/expo/expo-cli/pull/677

* Update Android SDK33 tarball with extra comments to remove unnecessary projects from settings.gradle, see https://github.com/expo/expo/pull/4478

* [changelog] Updated CHANGELOG
2019-06-07 15:16:33 +02:00
Wojciech KozyraandDominik Sokal 9cf607ef13 rename xdl imports to @expo/xdl (#73)
* rename xdl imports to @expo/xdl

* Update CHANGELOG.md

Co-Authored-By: Dominik Sokal <[email protected]>
2019-05-30 15:42:21 +02:00
Dominik Sokal 9287d1254e [k8s] wait only 20 minutes for any build to complete 2019-05-22 12:10:25 +02:00
Dominik Sokal 97977d895f tag metrics with message priority 2019-05-21 13:35:45 +02:00
Dominik Sokal 30d083ad62 fix initializing datadog api client 2019-05-20 13:33:33 +02:00
Dominik Sokal a3f01334df set DATADOG_API_KEY and DATADOG_APP_KEY in k8s pod 2019-05-20 11:43:10 +02:00
Dominik Sokal 65f1eea647 add sending builders config to datadog (#69)
* add sending builder config to datadog

* fix tests

* take all configurations if replica count not set
2019-05-20 11:33:48 +02:00
Dominik Sokal 2a739dc5ab add script for for setting builder replica count in redis (#68) 2019-05-17 12:49:00 +02:00
Wojciech Kozyra 201b28e6a8 upload logs to s3 on timeout (#66) 2019-05-16 13:24:21 +02:00
Dominik Sokal 55eaa18fde update CHANGELOG 2019-05-15 12:08:02 +02:00
Dominik Sokal 8ba0f61c6f update builders configuration 2019-05-15 12:05:23 +02:00
Dominik Sokal b9d0f12cac [turtle-cli] fix generating android keystore (#65) 2019-05-15 10:50:02 +02:00
TC Davis 6c2c55b650 increase priority workers 2019-05-13 14:55:14 -07:00
TC Davis 9e13991f63 increase priority workers 2019-05-13 10:31:00 -07:00
Stanisław Chmiela 2b7c7bb3ff [android] Update shell tarball for SDK32 with AppAuth fixes (#63) 2019-05-13 13:38:48 +02:00
Dominik Sokal 128371e452 change builders configuration 2019-05-10 10:03:48 +02:00
Dominik Sokal 19a668e541 Update CHANGELOG.md 2019-05-10 09:48:31 +02:00
Dominik Sokal 8751e7a8be Update CHANGELOG.md 2019-05-10 09:47:41 +02:00
Dominik Sokal 31dce0a9f5 move logo to the top 2019-05-09 16:18:48 +02:00
Zularizal 46a83d7c7c Add logo files (#61)
* Add logo files

* Update README.md
2019-05-09 16:12:14 +02:00
Dominik Sokal f0ecda40e8 Release 0.7.2 2019-05-09 09:40:47 +02:00
Dominik Sokal 3c92005a3d Update CHANGELOG.md 2019-05-09 09:39:49 +02:00
Dominik Sokal 9c6a3a4e39 change builders config (#58) 2019-05-09 09:38:10 +02:00
Wojciech Kozyra 71cd699488 remove client logger level env (#59)
* remove client logger level env

* Update CHANGELOG.md
2019-05-08 16:09:11 +02:00
Stanisław Chmiela 9480a51059 Add support for Android tarballs without universe-package.json (#53) 2019-05-08 12:39:12 +02:00
Dominik Sokal dd7a803116 upgrade @expo/traveling-fastlane-darwin to 1.9.4 2019-05-07 10:21:31 +02:00
Dominik Sokal 5f5cd6ac59 install curl and sed 2019-05-06 13:46:55 +02:00
Dominik Sokal 5552317fb8 Release 0.7.1 2019-05-06 12:00:27 +02:00
Dominik Sokal 4aebb52523 update CHANGELOG for 0.7.1 2019-05-06 11:59:38 +02:00
Dominik Sokal d22722bc33 set LC_ALL env when running fastlane 2019-05-06 11:57:57 +02:00
Dominik Sokal 7f432f453f Release 0.7.0 2019-05-06 11:12:12 +02:00
Dominik Sokal 44b2b0a9be update CHANGELOG for 0.7.0 2019-05-06 11:11:26 +02:00
Dominik Sokal 30789e939e refactor logger - brings back jobID and experienceName to android logs (#52) 2019-04-26 13:24:37 +02:00
Wojciech Kozyra 60a12f6710 Update CHANGELOG.md 2019-04-25 13:08:29 +02:00
Wojciech Kozyra 838f628ddd android custom builds (#47)
* android custom builds

* bump xdl version

* naming changes

* review fixes
2019-04-24 15:18:53 +02:00
Dominik Sokal 294a3df7b4 don't add ci/bin dir to path when in universe 2019-04-23 13:28:49 +02:00
Dominik Sokal 6bfcbbb663 update CHANGELOG for 0.6.1 2019-04-23 11:08:00 +02:00
Dominik Sokal 50e026d311 Release 0.6.1 2019-04-23 11:05:41 +02:00
Dominik Sokal 4dcb0b71be remove buildAndroidTarballLocally.sh scrip 2019-04-23 11:03:17 +02:00
Dominik Sokal 8bb527f04c update changelog 2019-04-19 16:39:08 +02:00
Dominik Sokal 1e7db2c92f update @expo-spawn-async to 1.4.2; update xdl to 53.5.3 2019-04-19 16:34:21 +02:00
Dominik Sokal 4d761dd293 record jobID and experience name in gcloud (#43)
# Why

To make it easier to find logs in GCloud Console.

# How

Basically, I copy-pasted `HackyLogglyStream` that we previously used in the project - https://github.com/expo/turtle/blob/de4837f36105c03be9afb4028241cff7459da974/src/logger/hackyLogglyStream.ts

# Test Plan

Tested locally.
<img width="487" alt="Screenshot 2019-04-15 at 15 09 37" src="https://user-images.githubusercontent.com/5256730/56135267-a3d3b200-5f90-11e9-9c96-505380b9f10c.png">
2019-04-18 09:32:33 +02:00
Dominik Sokal a40546606f update CHANGELOG 2019-04-12 13:31:48 +02:00
Dominik Sokal fbd7fcb4f8 [turtle-cli] check fastlane version before running build (#37) 2019-04-12 12:08:23 +02:00
Dominik Sokal a59ce7a7ee don't use babel for transpilation (#42)
With these changes, we don't need babel and gulp anymore. It should make developing turtle less frustrating.
2019-04-12 12:07:12 +02:00
Dominik Sokal e850bae696 remove node/npm/yarn from path when running turtle locally (#38)
We don't want to use node/npm/yarn from nix when we run turtle locally.
2019-04-12 10:16:24 +02:00
89 changed files with 3784 additions and 2357 deletions

No files matched your search

-16
View File
@@ -1,16 +0,0 @@
{
"presets": [
["env", {
"targets": {
"node": "8.9"
}
}]
],
"plugins": [
["module-resolver", {
"alias": {
"turtle": "./build"
}
}]
]
}
+2
View File
@@ -33,10 +33,12 @@ 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 \
+4 -5
View File
@@ -1,4 +1,4 @@
#!/bin/bash
#!/usr/bin/env bash
source_local() {
file=./.envrc.local
@@ -17,8 +17,7 @@ export EXPO_TURTLE_TOOLS_DIR="$(pwd)/tools"
if [ -z "$EXPO_UNIVERSE_DIR" ]; then
PATH_add $EXPO_TURTLE_TOOLS_DIR/bin
fi
if [[ $CI = "true" ]]; then
PATH_add $EXPO_TURTLE_TOOLS_DIR/ci/bin
if [ "$CI" = "true" ]; then
PATH_add $EXPO_TURTLE_TOOLS_DIR/ci/bin
fi
fi
+1
View File
@@ -2,6 +2,7 @@
node_modules
yarn-error.log
google-cloud-credentials.json
/build/
/workingdir/*
+1
View File
@@ -0,0 +1 @@
fixtures/
+82 -2
View File
@@ -5,12 +5,92 @@ 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.5] - 2019-06-19
### 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
- Updated iOS shell app for SDK 32 to fix AppAuth issue
- 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)).
## [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
+21 -25
View File
@@ -20,9 +20,11 @@ RUN dpkg --add-architecture i386 && \
libc6:i386\
libncurses5:i386\
libstdc++6:i386\
rsync\
unzip\
yarn\
zlib1g:i386\
supervisor\
--no-install-recommends && \
apt-get clean
@@ -50,20 +52,8 @@ ENV PATH ${ANDROID_HOME}/tools:${PATH}
ENV PATH ${ANDROID_HOME}/tools/bin:${PATH}
ENV PATH ${ANDROID_HOME}/build-tools/28.0.3/:${PATH}
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"
ADD scripts/android/configureAndroidSdk.sh /tmp/configureAndroidSdk.sh
RUN /tmp/configureAndroidSdk.sh --install-all-platforms
# Install Android NDK
ENV ANDROID_NDK_VERSION android-ndk-r10e
@@ -99,23 +89,29 @@ RUN rm gradle-4.4-all.zip
ENV GRADLE_HOME /usr/local/gradle-4.4
ENV PATH ${GRADLE_HOME}/bin:$PATH
# Install Gulp
RUN npm install -g gulp-cli
ADD . /app/turtle
ADD . /app
RUN for SDK_VERSION in `ls /app/workingdir/android/`; do \
RUN mv /app/turtle/workingdir /app && \
for SDK_VERSION in `ls /app/workingdir/android/`; do \
echo "preparing $SDK_VERSION shell app" && \
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 \
; done
WORKDIR /app
mv exponent-package.json package.json; \
else \
yarn install; \
fi \
; done && \
mv /app/workingdir /app/turtle
ENV NODE_ENV "production"
ENV TURTLE_WORKING_DIR_PATH /app/workingdir/
ENV TURTLE_WORKING_DIR_PATH /app/turtle/workingdir/
CMD ["node", "./build/server.js"]
WORKDIR /app/turtle
ADD supervisord.conf /etc/supervisor/conf.d/supervisord.conf
CMD ["/usr/bin/supervisord"]
+5 -1
View File
@@ -1,4 +1,6 @@
# Turtle CLI
<h3 align=center>
<img src="logo/horizontal.png" width=50%>
</h3>
[![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)
@@ -7,6 +9,8 @@
[![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.
+9
View File
@@ -1,6 +1,15 @@
#!/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
@@ -1,91 +0,0 @@
'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'));
+11 -1
View File
@@ -22,7 +22,7 @@ spec:
environment: ${NAMESPACE}
spec:
restartPolicy: Always
terminationGracePeriodSeconds: 1800 ## wait 30 minutes for any builds to complete
terminationGracePeriodSeconds: 1200 ## wait 20 minutes for any builds to complete
tolerations:
- key: dedicated
operator: Equal
@@ -128,6 +128,16 @@ 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.

After

Width:  |  Height:  |  Size: 69 KiB

+228
View File
@@ -0,0 +1,228 @@
<?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>

After

Width:  |  Height:  |  Size: 24 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

+6
View File
@@ -0,0 +1,6 @@
{
"watch": ["src"],
"ext": "ts",
"ignore": ["src/**/*.test.ts"],
"exec": "ts-node -r tsconfig-paths/register --files src/server.ts"
}
+30 -32
View File
@@ -1,20 +1,19 @@
{
"name": "turtle-cli",
"version": "0.6.0",
"version": "0.8.6",
"description": "The command-line tool for building standalone Expo apps",
"preferGlobal": true,
"scripts": {
"start": "EXPENV_OVERRIDES=local-common,local,local-queue expenv gulp",
"start": "EXPENV_OVERRIDES=local-common,local,local-queue expenv nodemon | bunyan -o short -l debug --color",
"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": "yarn build",
"build": "gulp build:server",
"prepare": "rimraf ./build && yarn build",
"build": "ttsc",
"build:production": "./scripts/build.sh",
"build:android-tarball:local": "./scripts/android/buildAndroidTarballLocally.sh",
"watch": "ttsc --watch",
"secrets:init-private": "./scripts/initPrivateSecrets.sh",
"set-workingdir:remote": "./scripts/setWorkingdirMode.sh remote",
"set-workingdir:local": "./scripts/setWorkingdirMode.sh local",
@@ -35,9 +34,14 @@
"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"
"set-priorities:default": "expenv ts-node -r tsconfig-paths/register --files src/scripts/setConfigurations.ts",
"set-priorities:local": "EXPENV_OVERRIDES=local-common,local,local-queue yarn set-priorities:default",
"set-priorities:staging": "yarn set-priorities:default",
"set-priorities:production": "EXPENV_OVERRIDES=prod yarn set-priorities:default",
"set-replica-count:default": "expenv ts-node -r tsconfig-paths/register --files src/scripts/setReplicaCount.ts | bunyan -o short -l debug",
"set-replica-count:local": "EXPENV_OVERRIDES=local-common,local,local-queue yarn set-replica-count:default",
"set-replica-count:staging": "yarn set-replica-count:default",
"set-replica-count:production": "EXPENV_OVERRIDES=prod yarn set-replica-count:default"
},
"husky": {
"hooks": {
@@ -56,7 +60,7 @@
"files": [
"bin",
"build",
"scripts/cli",
"scripts/android/configureAndroidSdk.sh",
"shellTarballs"
],
"keywords": [
@@ -80,41 +84,34 @@
"@types/fs-extra": "^5.0.4",
"@types/jest": "^23.3.9",
"@types/joi": "^13.4.3",
"@types/lodash": "^4.14.109",
"@types/lodash": "^4.14.129",
"@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.7",
"nodemon": "^1.18.11",
"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",
"typescript": "^3.0.1"
"ttypescript": "^1.5.6",
"typescript": "^3.4.2"
},
"dependencies": {
"@expo/spawn-async": "^1.4.0",
"@expo/spawn-async": "^1.4.2",
"@expo/xdl": "55.0.7",
"@google-cloud/logging-bunyan": "^0.9.5",
"async-retry": "^1.2.1",
"aws-sdk": "^2.226.1",
@@ -124,23 +121,24 @@
"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.10",
"lodash": "^4.17.11",
"progress": "^2.0.0",
"raven": "^2.6.2",
"request": "^2.88.0",
"request-progress": "^3.0.0",
"sharp": "^0.21.0",
"semver": "^6.0.0",
"sharp": "0.22.1",
"source-map-support": "^0.5.6",
"tar": "^4.4.6",
"uuid": "^3.2.1",
"which": "^1.3.1",
"xdl": "53.1.6-alpha.2"
"which": "^1.3.1"
},
"optionalDependencies": {
"@expo/traveling-fastlane-darwin": "^1.8.1"
"@expo/traveling-fastlane-darwin": "1.9.4"
}
}
@@ -1,32 +0,0 @@
#!/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
@@ -0,0 +1,20 @@
#!/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 ]]
+8
View File
@@ -0,0 +1,8 @@
#!/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
@@ -1,12 +0,0 @@
#!/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"
+13 -1
View File
@@ -36,7 +36,7 @@ setup_local() {
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")
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"
@@ -55,6 +55,18 @@ 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() {
+1
View File
@@ -21,5 +21,6 @@ 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,6 +19,7 @@ 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,5 +21,6 @@ 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
@@ -1 +0,0 @@
s3://exp-artifacts/android-shell-builder-c19ce0d88a153f481fd790e23bdcd6f2d5ea7fd6.tar.gz
+1 -1
View File
@@ -1 +1 @@
s3://exp-artifacts/android-shell-builder-0cab68670fc50ce1dc16b7d86ca7976870062c39.tar.gz
s3://exp-artifacts/android-shell-builder-f796aaffa0b5715079b2950faf26c2ff4e53e4c0.tar.gz
+1
View File
@@ -0,0 +1 @@
s3://exp-artifacts/android-shell-builder-5090aab941f92c912c6643a1e25cc69f9f67d8e6.tar.gz
+1 -1
View File
@@ -1 +1 @@
s3://exp-artifacts/ios-expo-client-15bb883c22173d5b01df19421580170e776041d0.tar.gz
s3://exp-artifacts/ios-expo-client-a2b1e16f330b36fa0dc627a76caabe4d2ff45f77.tar.gz
-1
View File
@@ -1 +0,0 @@
s3://exp-artifacts/ios-shell-builder-sdk-26-db356a09877642d64023af98e4e2e1e632fbe4df.tar.gz
+1
View File
@@ -0,0 +1 @@
s3://exp-artifacts/ios-shell-builder-sdk-latest-a2b1e16f330b36fa0dc627a76caabe4d2ff45f77.tar.gz
+25
View File
@@ -13,3 +13,28 @@ 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 },
],
];
+15 -18
View File
@@ -1,6 +1,5 @@
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';
@@ -15,18 +14,30 @@ 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 for your project, signed and ready for submission to the Google Play Store.',
'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.',
)
.asyncAction(
createBuilderAction({
program,
command,
prepareCredentials,
buildJobObject,
builder,
platform: PLATFORMS.ANDROID,
os: ['darwin', 'linux'],
@@ -34,20 +45,6 @@ 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 -19
View File
@@ -1,11 +1,10 @@
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, PLATFORMS } from 'turtle/constants';
import { IOS_BUILD_TYPES, PLATFORMS } from 'turtle/constants';
export default (program: any, setCommonCommandOptions: any) => {
const command = program.command('build:ios [project-dir]');
@@ -32,7 +31,6 @@ export default (program: any, setCommonCommandOptions: any) => {
program,
command,
prepareCredentials,
buildJobObject,
builder,
platform: PLATFORMS.IOS,
os: 'darwin',
@@ -40,23 +38,8 @@ 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;
}
+1 -1
View File
@@ -6,7 +6,7 @@ import setup from 'turtle/bin/setup/setup';
import { PLATFORMS } from 'turtle/constants';
import logger from 'turtle/logger';
const l = logger.withFields({ buildPhase: 'setting up environment' });
const l = logger.child({ buildPhase: 'setting up environment' });
function createSetupCommand(platform: string, os?: string) {
return (program: any) => {
+1 -1
View File
@@ -1,8 +1,8 @@
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 logger from 'turtle/logger';
+19 -15
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';
@@ -20,10 +20,6 @@ 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/',
@@ -41,7 +37,7 @@ const REQUIRED_TOOLS: IToolDefinition[] = [
},
];
const LOGGER_FIELDS = { buildPhase: 'setting up environment' };
const l = logger.withFields(LOGGER_FIELDS);
const l = logger.child(LOGGER_FIELDS);
export default async function setup(sdkVersion?: string) {
await checkSystem(REQUIRED_TOOLS);
@@ -67,18 +63,26 @@ function formatShellAppTarballUriPath(sdkMajorVersion: string) {
return path.join(config.directories.shellTarballsDir, PLATFORMS.ANDROID, `sdk${sdkMajorVersion}`);
}
async function _shellAppPostDownloadAction(workingdir: string) {
async function _shellAppPostDownloadAction(sdkVersion: string, workingdir: string) {
const inWorkingdir = (filename: string) => path.join(workingdir, filename);
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'));
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);
}
// TODO: remove following lines after upgrading android shell app
const toolsPublicDir = path.join(workingdir, 'tools-public');
await _installNodeModules(toolsPublicDir);
// 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);
}
}
async function _installNodeModules(cwd: string) {
+5 -3
View File
@@ -4,7 +4,7 @@ import path from 'path';
import decompress from 'decompress';
import fs from 'fs-extra';
import { formatArtifactDownloadPath } from 'turtle/bin/setup/utils/common';
import * as utils from 'turtle/bin/setup/utils/common';
import download from 'turtle/bin/setup/utils/downloader';
import config from 'turtle/config';
import logger from 'turtle/logger';
@@ -12,13 +12,14 @@ 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.withFields(LOGGER_FIELDS);
const l = logger.child(LOGGER_FIELDS);
export default async function ensureAndroidNDKIsPresent() {
const androidNdkDir = path.join(config.directories.androidDependenciesDir, 'ndk');
await utils.removeDirectoryUnlessReady(androidNdkDir);
if (!(await fs.pathExists(androidNdkDir))) {
await fs.ensureDir(androidNdkDir);
const androidNdkDownloadPath = formatArtifactDownloadPath(ANDROID_NDK_URL);
const androidNdkDownloadPath = utils.formatArtifactDownloadPath(ANDROID_NDK_URL);
try {
l.info('Downloading Android NDK');
@@ -26,6 +27,7 @@ export default async function ensureAndroidNDKIsPresent() {
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);
+7 -5
View File
@@ -1,11 +1,11 @@
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 { formatArtifactDownloadPath } from 'turtle/bin/setup/utils/common';
import * as utils from 'turtle/bin/setup/utils/common';
import download from 'turtle/bin/setup/utils/downloader';
import config from 'turtle/config';
import logger from 'turtle/logger';
@@ -13,13 +13,14 @@ 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.withFields(LOGGER_FIELDS);
const l = logger.child(LOGGER_FIELDS);
export default async function ensureAndroidSDKIsPresent() {
const androidSdkDir = path.join(config.directories.androidDependenciesDir, 'sdk');
await utils.removeDirectoryUnlessReady(androidSdkDir);
if (!(await fs.pathExists(androidSdkDir))) {
await fs.ensureDir(androidSdkDir);
const androidSdkDownloadPath = formatArtifactDownloadPath(ANDROID_SDK_URL);
const androidSdkDownloadPath = utils.formatArtifactDownloadPath(ANDROID_SDK_URL);
try {
l.info('Downloading Android SDK');
@@ -31,6 +32,7 @@ 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);
@@ -52,7 +54,7 @@ async function _configureSdk(androidSdkDir: string) {
await ExponentTools.spawnAsyncThrowError('./configureAndroidSdk.sh', [], {
pipeToLogger: true,
loggerFields: LOGGER_FIELDS,
cwd: path.join(config.directories.rootDir, 'scripts/cli'),
cwd: path.join(config.directories.rootDir, 'scripts/android'),
env: {
...process.env,
ANDROID_HOME: androidSdkDir,
+27 -1
View File
@@ -1,6 +1,9 @@
import last from 'lodash/last';
import trim from 'lodash/trim';
import path from 'path';
import semver from 'semver';
import { ExponentTools } from 'xdl';
import { ExponentTools } from '@expo/xdl';
import { checkSystem, ensureShellAppIsPresent } from 'turtle/bin/setup/utils/common';
import { IToolDefinition } from 'turtle/bin/setup/utils/toolsDetector';
@@ -16,6 +19,21 @@ 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',
@@ -47,3 +65,11 @@ export default async function setup(sdkVersion?: string) {
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;
}
+35 -4
View File
@@ -1,8 +1,8 @@
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';
@@ -10,14 +10,14 @@ import { IShellAppDirectoryConfig } from 'turtle/builders/utils/workingdir';
import config from 'turtle/config';
import logger from 'turtle/logger';
const l = logger.withFields({ buildPhase: 'setting up environment' });
const l = logger.child({ buildPhase: 'setting up environment' });
interface IShellAppFormaters {
formatShellAppDirectory: (config: IShellAppDirectoryConfig) => string;
formatShellAppTarballUriPath: (sdkMajorVersion: string) => string;
}
type PostDownloadAction = (workingdir: string) => void;
type PostDownloadAction = (sdkVersion: string, workingdir: string) => void;
export async function checkSystem(requiredTools: IToolDefinition[]) {
await ensureToolsAreInstalled(requiredTools);
@@ -29,14 +29,17 @@ export async function ensureShellAppIsPresent(
postDownloadAction?: PostDownloadAction,
) {
const workingdir = formatters.formatShellAppDirectory({ sdkVersion });
const shellAppMetadata = await _readShellAppTarballS3Uri(sdkVersion, formatters);
await removeDirectoryUnlessReady(workingdir, shellAppMetadata);
if (await fs.pathExists(workingdir)) {
return;
}
l.info(`shell app for SDK ${sdkVersion} doesn't exist, downloading...`);
await _downloadShellApp(sdkVersion, workingdir, formatters);
if (postDownloadAction) {
await postDownloadAction(workingdir);
await postDownloadAction(sdkVersion, workingdir);
}
await markDirectoryAsReady(workingdir, shellAppMetadata);
}
async function _downloadShellApp(sdkVersion: string, targetDirectory: string, formatters: IShellAppFormaters) {
@@ -65,3 +68,31 @@ 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);
}
}
+11 -2
View File
@@ -4,17 +4,18 @@ import _which from 'which';
import logger from 'turtle/logger';
const which = util.promisify(_which);
const l = logger.withFields({ buildPhase: 'setting up environment' });
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[]) {
let isAnyToolMissing = false;
for (const { command, missingDescription, testFn } of tools) {
for (const { command, missingDescription, testFn, versionCheckFn } of tools) {
try {
if (testFn) {
if (!await testFn()) {
@@ -30,6 +31,14 @@ export async function ensureToolsAreInstalled(tools: IToolDefinition[]) {
}
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.');
+40 -2
View File
@@ -1,10 +1,13 @@
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';
@@ -12,7 +15,6 @@ export function createBuilderAction({
program,
command,
prepareCredentials,
buildJobObject,
builder,
platform,
os,
@@ -55,6 +57,7 @@ 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,7 +68,7 @@ export function createBuilderAction({
await setup(platform, sdkVersion);
const credentials = await prepareCredentials(cmd);
const rawJob = {
...buildJobObject(appJSON, args, credentials),
...await buildJobObject(platform, appJSON, args, credentials),
...cmd.buildDir && { fakeUploadDir: ProjectUtils.resolveAbsoluteDir(cmd.buildDir) },
...cmd.output && { fakeUploadBuildPath: ProjectUtils.resolveAbsoluteDir(cmd.output) },
};
@@ -82,3 +85,38 @@ 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 'xdl';
import { User as UserManager } from '@expo/xdl';
import { ErrorWithProgramHelp } from 'turtle/bin/commands/ErrorWithProgramHelp';
+21 -6
View File
@@ -1,14 +1,17 @@
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';
@@ -18,13 +21,16 @@ import logger from 'turtle/logger';
export default async function buildAndroid(jobData: IJob): Promise<IJobResult> {
await ensureCanBuildSdkVersion(jobData);
const credentials = await getOrCreateCredentials(jobData);
const apkFilePath = await runShellAppBuilder(jobData, credentials);
const outputFilePath = await runShellAppBuilder(jobData, credentials);
const randomHex = uuidv4().replace(/-/g, '');
const s3Filename = `${jobData.experienceName}-${randomHex}-signed.apk`;
const s3FileExtension = jobData.config.buildType === ANDROID_BUILD_TYPES.APP_BUNDLE ? 'aab' : 'apk';
const s3Filename = `${jobData.experienceName}-${randomHex}-signed.${s3FileExtension}`;
const s3FileKey = `android/${s3Filename}`;
const fakeUploadFilename = s3Filename.replace('/', '\\');
const artifactUrl = await uploadBuildToS3({
uploadPath: apkFilePath,
uploadPath: outputFilePath,
s3FileKey,
...config.builder.fakeUpload && {
fakeUploadBuildPath:
@@ -60,15 +66,21 @@ async function runShellAppBuilder(
ImageUtils.setResizeImageFunction(imageHelpers.resizeIconWithSharpAsync);
ImageUtils.setGetImageDimensionsFunction(imageHelpers.getImageDimensionsWithSharpAsync);
const outputFilePath = path.join(temporaryFilesRoot, `shell-signed-${jobData.id}.apk`);
const fileExtension = jobData.config.buildType === ANDROID_BUILD_TYPES.APP_BUNDLE ? 'aab' : 'apk';
const outputFilePath = path.join(temporaryFilesRoot, `shell-signed-${jobData.id}.${fileExtension}`);
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),
url: commonUtils.getExperienceUrl(jobData.experienceName, jobData.config.publicUrl),
sdkVersion,
keystore: tempKeystorePath,
manifest,
@@ -79,6 +91,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);
+2 -2
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';
@@ -29,7 +29,7 @@ export default async function buildArchive(ctx: IContext, job: IJob) {
}
async function buildAndSignIPA(ctx: IContext, job: IJob, keychainPath: string, manifest: any) {
const l = logger.withFields({ buildPhase: 'building and signing IPA' });
const l = logger.child({ buildPhase: 'building and signing IPA' });
l.info('building and signing IPA');
const {
+7 -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, PLATFORMS } from 'turtle/constants/index';
import { IOS_BUILD_TYPES, PLATFORMS } from 'turtle/constants/index';
import { IJob } from 'turtle/job';
export interface IContext {
@@ -27,7 +27,6 @@ export interface IContext {
}
const { EXPOKIT_APP, EXPONENT_APP } = IosShellApp;
const { BUILD_TYPES } = IOS;
export function createBuilderContext(job: IJob): IContext {
const { join } = path;
@@ -57,7 +56,7 @@ export function createBuilderContext(job: IJob): IContext {
context.tempCertPath = join(context.appDir, 'cert.p12');
context.baseArchiveDir = join(context.appDir, 'archive');
if (buildType === BUILD_TYPES.CLIENT) {
if (buildType === IOS_BUILD_TYPES.CLIENT) {
context.applicationFilesSrc = join(
workingDir,
'expo-client-build',
@@ -74,7 +73,7 @@ export function createBuilderContext(job: IJob): IContext {
);
}
if (buildType === BUILD_TYPES.ARCHIVE) {
if (buildType === IOS_BUILD_TYPES.ARCHIVE) {
context.outputPath = join(context.appDir, 'archive.xcarchive');
context.uploadPath = join(context.buildDir, 'archive.ipa');
context.archiveDir = join(
@@ -92,7 +91,7 @@ export function createBuilderContext(job: IJob): IContext {
'default',
`${EXPOKIT_APP}.xcworkspace`,
);
} else if (buildType === BUILD_TYPES.CLIENT) {
} else if (buildType === IOS_BUILD_TYPES.CLIENT) {
context.outputPath = join(context.appDir, 'archive.xcarchive');
context.uploadPath = join(context.buildDir, 'archive.ipa');
context.archiveDir = join(
@@ -107,7 +106,7 @@ export function createBuilderContext(job: IJob): IContext {
'ios',
`${EXPONENT_APP}.xcworkspace`,
);
} else if (buildType === BUILD_TYPES.SIMULATOR) {
} else if (buildType === IOS_BUILD_TYPES.SIMULATOR) {
context.outputPath = join(context.appDir, 'archive.tar.gz');
context.uploadPath = join(context.appDir, 'archive.tar.gz');
context.archiveDir = join(
@@ -117,7 +116,7 @@ export function createBuilderContext(job: IJob): IContext {
);
}
const s3FileExtension = buildType === BUILD_TYPES.SIMULATOR ? 'tar.gz' : 'ipa';
const s3FileExtension = buildType === IOS_BUILD_TYPES.SIMULATOR ? 'tar.gz' : 'ipa';
const s3Filename = `${job.experienceName}-${appUUID}-${buildType}.${s3FileExtension}`;
if (config.builder.fakeUpload) {
const fakeUploadFilename = s3Filename.replace('/', '\\');
+5 -7
View File
@@ -9,13 +9,11 @@ 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 } from 'turtle/constants/index';
import { IOS_BUILD_TYPES } from 'turtle/constants';
import { IJob, IJobResult } from 'turtle/job';
const { BUILD_TYPES } = IOS;
export default async function iosBuilder(job: IJob): Promise<IJobResult> {
if (job.config.buildType !== BUILD_TYPES.CLIENT) {
if (job.config.buildType !== IOS_BUILD_TYPES.CLIENT) {
await ensureCanBuildSdkVersion(job);
}
@@ -26,13 +24,13 @@ export default async function iosBuilder(job: IJob): Promise<IJobResult> {
const { buildType } = job.config;
if (buildType === BUILD_TYPES.CLIENT) {
if (buildType === IOS_BUILD_TYPES.CLIENT) {
await prepareAdHocBuildCredentials(job);
}
if ([BUILD_TYPES.ARCHIVE, BUILD_TYPES.CLIENT].includes(buildType!)) {
if ([IOS_BUILD_TYPES.ARCHIVE, IOS_BUILD_TYPES.CLIENT].includes(buildType!)) {
await buildArchive(ctx, job);
} else if (buildType === BUILD_TYPES.SIMULATOR) {
} else if (buildType === IOS_BUILD_TYPES.SIMULATOR) {
await buildSimulator(ctx, job);
} else {
throw new Error(`Unsupported iOS build type: ${buildType}`);
+1 -1
View File
@@ -4,7 +4,7 @@ import { IJob } from 'turtle/job';
import logger from 'turtle/logger';
export default async function buildSimulator(ctx: IContext, job: IJob) {
const l = logger.withFields({ buildPhase: 'running simulator builder' });
const l = logger.child({ buildPhase: 'running simulator builder' });
l.info('running simulator build');
await runShellAppBuilder(ctx, job);
l.info('build complete');
+11 -3
View File
@@ -1,8 +1,9 @@
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';
@@ -12,14 +13,21 @@ import { isOffline } from 'turtle/turtleContext';
async function getOrCreateCredentials(jobData: IJob): Promise<IAndroidCredentials> {
if (!jobData.credentials) {
const l = logger.withFields({ buildPhase: 'generating keystore' });
const l = logger.child({ buildPhase: 'generating keystore' });
const credentials: any = {};
l.info('Creating keystore');
const keystoreFilename = `/tmp/keystore-${jobData.id}.tmp.jks`;
credentials.keystorePassword = uuidv4().replace(/-/g, '');
credentials.keyPassword = uuidv4().replace(/-/g, '');
credentials.keystoreAlias = Buffer.from(jobData.experienceName).toString('base64');
const androidPackage = jobData.manifest.android.package;
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).',
);
}
await Credentials.Android.createKeystore(
{
+1 -1
View File
@@ -1,6 +1,6 @@
import path from 'path';
import { ExponentTools } from 'xdl';
import { ExponentTools } from '@expo/xdl';
import { IShellAppDirectoryConfig } from 'turtle/builders/utils/workingdir';
import config from 'turtle/config';
+1 -5
View File
@@ -1,13 +1,9 @@
import * as url from 'url';
import config from 'turtle/config';
import { IJob } from 'turtle/job';
import logger from 'turtle/logger';
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;
+1 -1
View File
@@ -1,9 +1,9 @@
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';
+4 -4
View File
@@ -1,4 +1,4 @@
import { IosKeychain } from 'xdl';
import { IosKeychain } from '@expo/xdl';
import { IContext } from 'turtle/builders/ios/context';
import * as fileUtils from 'turtle/builders/utils/file';
@@ -9,7 +9,7 @@ interface IKeychain {
}
export async function create(ctx: IContext): Promise<IKeychain> {
const l = logger.withFields({ buildPhase: 'creating keychain' });
const l = logger.child({ buildPhase: 'creating keychain' });
try {
l.info('creating keychain...');
const keychainInfo = await IosKeychain.createKeychain(ctx.appUUID, false);
@@ -22,7 +22,7 @@ export async function create(ctx: IContext): Promise<IKeychain> {
}
export async function remove(ctx: IContext, keychainPath: string) {
const l = logger.withFields({ buildPhase: 'deleting keychain' });
const l = logger.child({ buildPhase: 'deleting keychain' });
try {
l.info('delete keychain...');
const keychainInfo = await IosKeychain.deleteKeychain({
@@ -49,7 +49,7 @@ export async function importCert(
certPassword,
}: { keychainPath: string; certP12: string; certPassword: string },
) {
const l = logger.withFields({ buildPhase: 'importing certificate into keychain' });
const l = logger.child({ buildPhase: 'importing certificate into keychain' });
try {
l.info('importing distribution certificate into keychain...');
const { tempCertPath: certPath } = ctx;
+4 -4
View File
@@ -1,14 +1,14 @@
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 } from 'turtle/constants/index';
import { IOS_BUILD_TYPES } from 'turtle/constants/index';
import { IJob } from 'turtle/job';
import logger from 'turtle/logger/index';
@@ -37,13 +37,13 @@ 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),
url: commonUtils.getExperienceUrl(job.experienceName, job.config.publicUrl),
releaseChannel,
sdkVersion,
});
+3 -3
View File
@@ -1,14 +1,14 @@
import path from 'path';
import { ExponentTools } from 'xdl';
import { ExponentTools } from '@expo/xdl';
import { IShellAppDirectoryConfig } from 'turtle/builders/utils/workingdir';
import config from 'turtle/config';
import { IOS, PLATFORMS } from 'turtle/constants';
import { IOS_BUILD_TYPES, 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
@@ -0,0 +1,169 @@
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;
}
+2 -2
View File
@@ -12,14 +12,14 @@ interface IUploadCtx {
export async function uploadBuildToS3(ctx: IUploadCtx) {
if (config.builder.fakeUpload) {
const l = logger.withFields({ buildPhase: 'copying build artifact' });
const l = 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.withFields({ buildPhase: 'uploading to S3' });
const l = logger.child({ buildPhase: 'uploading to S3' });
l.info('uploading build artifact to S3');
const { Location: fileLocation } = await uploadFile({
key: ctx.s3FileKey as string,
+1 -1
View File
@@ -1,6 +1,6 @@
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';
+2 -2
View File
@@ -1,6 +1,6 @@
import { IOS } from 'turtle/constants';
import { ANDROID_BUILD_TYPES, IOS_BUILD_TYPES } from 'turtle/constants';
export interface IShellAppDirectoryConfig {
sdkVersion: string;
buildType?: IOS.BUILD_TYPES;
buildType?: IOS_BUILD_TYPES & ANDROID_BUILD_TYPES;
}
+9 -6
View File
@@ -2,7 +2,7 @@ import os from 'os';
import path from 'path';
import { PLATFORMS } from 'turtle/constants';
import { env, envNum, envOptional, envTransform } from 'turtle/utils/env';
import { env, envBoolean, envNum, envOptional, envTransform } from 'turtle/utils/env';
export default {
env: env('NODE_ENV'),
@@ -37,16 +37,19 @@ 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'),
interval: envNum('LOGGER_INTERVAL', 5),
client: {
level: env('CLIENT_LOGGER_LEVEL', 'info'),
},
intervalMs: envNum('LOGGER_INTERVAL_MS', 5000),
},
google: {
credentials: env('GOOGLE_APPLICATION_CREDENTIALS', ''),
@@ -67,7 +70,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', 60 * 60 * 1000),
maxJobTimeMs: envNum('TURTLE_MAX_JOB_TIME_MS', 15 * 60 * 1000),
useLocalWorkingDir: envTransform('TURTLE_USE_LOCAL_WORKING_DIR', '0', (val: string) => val === '1'),
},
directories: {
+17 -2
View File
@@ -1,9 +1,24 @@
import * as BUILD from './build';
import * as IOS from './ios';
export enum PLATFORMS {
IOS = 'ios',
ANDROID = 'android',
}
export { IOS, BUILD };
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 };
-5
View File
@@ -1,5 +0,0 @@
export enum BUILD_TYPES {
ARCHIVE = 'archive',
CLIENT = 'client',
SIMULATOR = 'simulator',
}
+5 -2
View File
@@ -1,4 +1,4 @@
import { IOS } from 'turtle/constants';
import { ANDROID_BUILD_MODES, ANDROID_BUILD_TYPES, IOS_BUILD_TYPES } from 'turtle/constants';
export interface IJob {
platform: 'android' | 'ios';
@@ -9,9 +9,12 @@ 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
+10 -6
View File
@@ -64,7 +64,10 @@ async function processJob(jobData: any) {
setCurrentJobId(job.id);
const pingerHandle = sqs.changeMessageVisibilityRecurring(priority, jobData.ReceiptHandle, job.id);
logger.info(`Doing job MessageId=${jobData.MessageId} BuildId=${job.id} ${Date.now()}`);
logger.info(
`Doing job MessageId=${jobData.MessageId} BuildId=${job.id} `
+ `Priority=${priority.slice(0, priority.length - 8)} ${Date.now()}`,
);
const cancelled = await redis.checkIfCancelled(job.id);
if (cancelled) {
@@ -76,10 +79,10 @@ async function processJob(jobData: any) {
const buildType = _.get(job, 'config.buildType', 'default');
try {
const status = await build(job);
buildStatusMetric.add(buildType, status);
buildStatusMetric.add(buildType, priority, status);
buildFailed = !status;
} catch (err) {
buildStatusMetric.add(buildType, false);
buildStatusMetric.add(buildType, priority, false);
buildFailed = true;
throw err;
} finally {
@@ -113,7 +116,8 @@ function failAfterMaxJobTime(priority: string, receiptHandle: string, job: any)
sqs.sendMessage(job.id, BUILD.JOB_STATES.ERRORED, { turtleVersion });
await deleteMessage(priority, receiptHandle);
} finally {
logger.error('Going to terminate turtle agent, just in case...');
logger.error('Build timed out. Going to terminate turtle agent.');
await logger.cleanup();
process.exit(1);
}
}, config.builder.maxJobTimeMs);
@@ -121,7 +125,7 @@ function failAfterMaxJobTime(priority: string, receiptHandle: string, job: any)
async function build(job: any) {
const startTimestamp = Date.now();
const s3Url = await logger.init(job);
const s3Url = await logger.initForJob(job);
const calculateBuildDuration = () => Math.ceil((Date.now() - startTimestamp) / 1000);
try {
@@ -151,7 +155,7 @@ async function build(job: any) {
});
return false;
} finally {
await logger.cleanup(job);
await logger.cleanup();
}
}
+5 -1
View File
@@ -1,6 +1,6 @@
import Joi from 'joi';
import { PLATFORMS } from 'turtle/constants';
import { ANDROID_BUILD_MODES, ANDROID_BUILD_TYPES, PLATFORMS } from 'turtle/constants';
import baseJobSchema from 'turtle/jobsSchemas/base';
export default baseJobSchema.concat(
@@ -12,5 +12,9 @@ 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 -3
View File
@@ -1,13 +1,13 @@
import Joi from 'joi';
import { IOS, PLATFORMS } from 'turtle/constants/index';
import { IOS_BUILD_TYPES, 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()
@@ -21,7 +21,7 @@ export default baseJobSchema.concat(
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(),
}),
+65 -81
View File
@@ -1,92 +1,76 @@
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 * as constants from 'turtle/constants/logger';
import { IJob } from 'turtle/job';
import S3Stream from 'turtle/logger/s3Stream';
import { isOffline } from 'turtle/turtleContext';
import streams from 'turtle/logger/streams';
// 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;
interface ILoggerFields {
[key: string]: string | number;
}
export const s3logger = new S3Stream();
class Logger {
private parentLogger: any;
private currentLogger: any;
const streams: IStream[] = [];
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;
}
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,
},
);
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 (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.withFields = (extraFields: any) => withFields(logger, extraFields);
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;
export function withFields(loggerObj: any, extraFields: any) {
return constants.LEVELS.reduce((obj, level) => {
obj[level] = (...args: any[]) => loggerObj[level](extraFields, ...args);
return obj;
}, {} as any);
}
export default new Logger();
-111
View File
@@ -1,111 +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 * 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
@@ -0,0 +1,29 @@
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
@@ -0,0 +1,38 @@
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
@@ -0,0 +1,11 @@
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
@@ -0,0 +1,106 @@
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
@@ -0,0 +1,18 @@
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),
};
}
+3 -3
View File
@@ -1,5 +1,5 @@
import * as cloudwatch from 'turtle/aws/cloudwatch';
import { TYPE_DIMENSIONS } from 'turtle/aws/cloudwatch/dimensions';
import { PRIORITY_DIMENSIONS } from 'turtle/aws/cloudwatch/dimensions';
import * as reducers from 'turtle/aws/cloudwatch/reducers';
import config from 'turtle/config';
import logger from 'turtle/logger';
@@ -24,11 +24,11 @@ export function register() {
});
}
export function add(type: string, success = true) {
export function add(type: string, priority: string, success = true) {
cloudwatch.addMetricData({
name: BUILD_METRIC_NAME,
value: 1,
success,
extraDimensions: TYPE_DIMENSIONS(type),
extraDimensions: PRIORITY_DIMENSIONS(type, priority),
});
}
+54
View File
@@ -0,0 +1,54 @@
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() {
const configuration = await getLabeledConfiguration();
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
@@ -0,0 +1,24 @@
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 || [])] });
}
}
@@ -1,12 +1,13 @@
import { env } from 'turtle/utils/env';
import { connect, RedisClient } from 'turtle/utils/redis';
import { PLATFORMS } from 'turtle/constants';
import logger from 'turtle/logger';
import {
createConfigurationsKey,
createDefaultConfigurationKey,
HIGH_CONFIGURATION,
NORMAL_CONFIGURATION,
HIGH_ONLY_CONFIGURATION,
} from 'turtle/utils/priorities';
import { connect, RedisClient } from 'turtle/utils/redis';
const TIMEOUT = 3000;
@@ -25,36 +26,40 @@ const TIMEOUT = 3000;
// 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_ONLY_CONFIGURATION,
HIGH_ONLY_CONFIGURATION,
HIGH_ONLY_CONFIGURATION,
// there are 5 iOS builders at the moment
HIGH_CONFIGURATION,
HIGH_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,
HIGH_CONFIGURATION,
HIGH_CONFIGURATION,
HIGH_CONFIGURATION,
HIGH_CONFIGURATION,
HIGH_CONFIGURATION,
HIGH_CONFIGURATION,
HIGH_CONFIGURATION,
// there are 15 Android builders at the moment
HIGH_CONFIGURATION,
HIGH_CONFIGURATION,
HIGH_CONFIGURATION,
];
const DEFAULT_CONFIGURATION = HIGH_CONFIGURATION;
async function run() {
@@ -68,6 +73,8 @@ async function run() {
redis.disconnect();
}
run()
.then(() => console.log('All done'))
.catch(err => console.error('Error:', err));
if (require.main === module) {
run()
.then(() => logger.info('All done'))
.catch((err) => logger.error('Error:', err));
}
+38
View File
@@ -0,0 +1,38 @@
import isNaN from 'lodash/isNaN';
import { PLATFORMS } from 'turtle/constants';
import logger from 'turtle/logger';
import { setReplicaCount } from 'turtle/utils/metadata';
import { getRedisClient, RedisClient } from 'turtle/utils/redis';
async function run() {
const platform = process.env.SCRIPT_PLATFORM;
const replicaCountRaw = process.env.SCRIPT_REPLICA_COUNT;
const replicaCount = Number(replicaCountRaw);
if (!platform || !replicaCountRaw) {
throw new Error('You must set both SCRIPT_PLATFORM and SCRIPT_REPLICA_COUNT env variables.');
}
if (!Object.values(PLATFORMS).includes(platform)) {
throw new Error(`Unsupported platform: ${platform}`);
}
if (isNaN(replicaCount)) {
throw new Error(`Wrong value for replica count: ${replicaCountRaw}`);
}
const redis = await getRedisClient(RedisClient.Configuration);
logger.info(`[${platform}] Setting replica count in redis to ${replicaCount}`);
await setReplicaCount(platform as PLATFORMS, replicaCount);
redis.disconnect();
}
if (require.main === module) {
run()
.then(() => logger.info('All done'))
.catch((err) => {
logger.error('Error:', err);
process.exit(1);
});
}
+1 -1
View File
@@ -1,4 +1,4 @@
import { LoggerDetach } from 'xdl';
import { LoggerDetach } from '@expo/xdl';
import config from 'turtle/config';
import { doJob } from 'turtle/jobManager';
+13
View File
@@ -1,10 +1,23 @@
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();
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);
}
}
+5 -4
View File
@@ -1,9 +1,10 @@
declare module 'copy';
declare module 'xdl';
declare module 'async-retry';
declare module 'bunyan';
declare module 'bunyan-sentry-stream';
declare module 'bunyan-debug-stream';
declare module 'async-retry';
declare module 'copy';
declare module 'dogapi';
declare module 'ioredis';
declare module 'request-progress';
declare module 'is-semver';
declare module 'request-progress';
declare module '@expo/xdl';
+11
View File
@@ -16,5 +16,16 @@ 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';
+21
View File
@@ -0,0 +1,21 @@
import config from 'turtle/config';
import { PLATFORMS } from 'turtle/constants';
import { getRedisClient, RedisClient } from 'turtle/utils/redis';
const createKeyPrefix = (platform: PLATFORMS) => `${config.deploymentEnv}:${platform}`;
const createReplicaCountKey = (platform: PLATFORMS) => `${createKeyPrefix(platform)}:replicaCount`;
export async function setReplicaCount(platform: PLATFORMS, replicaCount: number) {
const redis = await getRedisClient(RedisClient.Configuration);
await redis.set(
createReplicaCountKey(platform),
replicaCount,
);
}
export async function getReplicaCount(maybePlatform?: PLATFORMS) {
const platform = maybePlatform || config.platform;
const redis = await getRedisClient(RedisClient.Configuration);
const result = await redis.get(createReplicaCountKey(platform));
return result === null ? null : Number(result);
}
+27 -1
View File
@@ -1,4 +1,5 @@
import Joi from 'joi';
import isEqual from 'lodash/isEqual';
import os from 'os';
import config from 'turtle/config';
@@ -8,8 +9,15 @@ import { getRedisClient, RedisClient } from 'turtle/utils/redis';
export const NORMAL = 'normalPriority';
export const HIGH = 'highPriority';
export enum TurtleModeLabels {
Normal = 'normal',
High = 'high',
HighOnly = 'highOnly',
}
export const NORMAL_CONFIGURATION = [NORMAL, HIGH];
export const HIGH_CONFIGURATION = [HIGH, NORMAL];
export const HIGH_ONLY_CONFIGURATION = [HIGH];
const REDIS_GET_CONFIG = `
local i = 0
@@ -55,7 +63,7 @@ export async function getPriorities() {
}
try {
const configurations = JSON.parse(await redis.get(createConfigurationsKey()));
const configurations = await getConfiguration(redis);
const configurationsIndex = await redis.getConfig(createConfigurationsKey(), os.hostname());
logger.debug(`Using configuration at index ${configurationsIndex} pulled from redis`);
const { error, value: configValue } = configSchema.validate(configurations[configurationsIndex]);
@@ -84,3 +92,21 @@ export async function getPriorities() {
}
}
}
export async function getConfiguration(currentRedisClient: any) {
const redis = currentRedisClient || await getRedisClient(RedisClient.Configuration);
return JSON.parse(await redis.get(createConfigurationsKey()));
}
export async function getLabeledConfiguration(currentRedisClient?: any) {
const configuration = await getConfiguration(currentRedisClient);
return configuration.map((i: string[]) => {
if (isEqual(i, NORMAL_CONFIGURATION)) {
return TurtleModeLabels.Normal;
} else if (isEqual(i, HIGH_CONFIGURATION)) {
return TurtleModeLabels.High;
} else {
return TurtleModeLabels.HighOnly;
}
});
}
+2 -1
View File
@@ -20,7 +20,7 @@ const clientToUrl: IClientConfig = {
[RedisClient.Configuration]: config.redis.configUrl,
};
export function connect(timeoutMs: number, type: string) {
export function connect(timeoutMs: number, type: string): Promise<any> {
return new Promise((resolve, reject) => {
const redisClient = new Redis(clientToUrl[type], {
maxRetriesPerRequest: 2,
@@ -79,6 +79,7 @@ export async function registerListener(jobId: string, deleteMessage: any) {
if (message === jobId) {
logger.info({ lastBuildLog: true }, 'Job cancelled - killing process');
await deleteMessage();
await logger.cleanup();
setTimeout(() => process.exit(1), MILLIS_TO_UPLOAD_LOGS);
}
});
+17
View File
@@ -0,0 +1,17 @@
[supervisord]
nodaemon=true
[program:turtle]
directory=/app/turtle
command=node ./build/server.js
autorestart=true
startretries=5
startsecs=600
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0
[eventlistener:turtle_failure]
events=PROCESS_STATE_FATAL
command=/app/turtle/scripts/android/supervisordEventListener.sh
+6 -3
View File
@@ -6,17 +6,20 @@
"sourceMap": true,
"inlineSources": true,
"outDir": "./build",
"rootDir": "./src",
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"esModuleInterop": true,
"baseUrl": "./",
"baseUrl": ".",
"paths": {
"turtle/*": ["src/*"],
},
"allowJs": true
"allowJs": true,
"plugins": [
{ "transform": "ts-transformer-imports" }
]
},
"include": ["src"],
"exclude": ["node_modules", "build", "workingdir"]
}
+2348 -1764
View File
File diff suppressed because it is too large. Load diff