mirror of
https://github.com/expo/turtle.git
synced 2026-11-14 19:12:55 -10:00
Compare commits
223
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9d8a046dd4 | ||
|
|
d29d2b8de0 | ||
|
|
2b8548f489 | ||
|
|
44b0d8c696 | ||
|
|
9b70b047a3 | ||
|
|
98310af75a | ||
|
|
5f9bf0e735 | ||
|
|
32c8032bb3 | ||
|
|
10322bbca1 | ||
|
|
88ca49766a | ||
|
|
0d22f3ed0f | ||
|
|
32d104a03b | ||
|
|
97422aeb54 | ||
|
|
7a80d92e72 | ||
|
|
1a2a7654c2 | ||
|
|
a5aaa46790 | ||
|
|
1691655f61 | ||
|
|
f38ab7c9ee | ||
|
|
8fd8a6e70e | ||
|
|
43a319b12b | ||
|
|
511f58e6ec | ||
|
|
350f68e330 | ||
|
|
851b0a62ab | ||
|
|
d97f290ce1 | ||
|
|
2feb10aeec | ||
|
|
8a288c271a | ||
|
|
47fe68abbd | ||
|
|
51d00a8706 | ||
|
|
3f25edccd7 | ||
|
|
2f10ab1bc9 | ||
|
|
4f8c21a075 | ||
|
|
7179f6019c | ||
|
|
2bbf2d7784 | ||
|
|
3ccb4c994e | ||
|
|
46706a7cdb | ||
|
|
383358b11d | ||
|
|
a87cc75b5d | ||
|
|
391a3a9937 | ||
|
|
7cfedbc448 | ||
|
|
9b6eb8b66a | ||
|
|
c52a126409 | ||
|
|
908ab0f74d | ||
|
|
ab15d4c407 | ||
|
|
9f7b77a09c | ||
|
|
67260647ff | ||
|
|
849759c6c5 | ||
|
|
fdcf67c96f | ||
|
|
407db0efb6 | ||
|
|
8ec619a627 | ||
|
|
849e026833 | ||
|
|
13652b0e07 | ||
|
|
06c2c71515 | ||
|
|
5f98e9f5ce | ||
|
|
f314ef17dd | ||
|
|
30274bfd7e | ||
|
|
8ef99f2eae | ||
|
|
855e4bb962 | ||
|
|
df789b2de7 | ||
|
|
eff0556291 | ||
|
|
db74843ae0 | ||
|
|
a0e1382531 | ||
|
|
952dcd0cd3 | ||
|
|
0cd4e2953a | ||
|
|
a03d93cb27 | ||
|
|
010e92472b | ||
|
|
75316b5f07 | ||
|
|
4baa49e8fe | ||
|
|
edb167e6b1 | ||
|
|
d552b97654 | ||
|
|
12718c0b36 | ||
|
|
f9600f26e8 | ||
|
|
68f9744f7b | ||
|
|
8c2031c3d7 | ||
|
|
3165b5346e | ||
|
|
1552c9e7f2 | ||
|
|
51a0e772c0 | ||
|
|
98747c1db6 | ||
|
|
4f64aa2617 | ||
|
|
1f487d39fb | ||
|
|
f7e647c1f4 | ||
|
|
d900e3c491 | ||
|
|
32b049df86 | ||
|
|
723c84b575 | ||
|
|
0ab4d2ed90 | ||
|
|
05b96a5b6f | ||
|
|
5467aca0fb | ||
|
|
cf1e1daa26 | ||
|
|
8649968531 | ||
|
|
b89214fcc9 | ||
|
|
0142147a61 | ||
|
|
331c8edba7 | ||
|
|
ed382e0d2c | ||
|
|
3f0958845d | ||
|
|
6e345757e3 | ||
|
|
3588c5a5c4 | ||
|
|
1f94e38e60 | ||
|
|
ca8465d2a1 | ||
|
|
fe31b74471 | ||
|
|
ed12f3c34b | ||
|
|
9cf607ef13 | ||
|
|
9287d1254e | ||
|
|
97977d895f | ||
|
|
30d083ad62 | ||
|
|
a3f01334df | ||
|
|
65f1eea647 | ||
|
|
2a739dc5ab | ||
|
|
201b28e6a8 | ||
|
|
55eaa18fde | ||
|
|
8ba0f61c6f | ||
|
|
b9d0f12cac | ||
|
|
6c2c55b650 | ||
|
|
9e13991f63 | ||
|
|
2b7c7bb3ff | ||
|
|
128371e452 | ||
|
|
19a668e541 | ||
|
|
8751e7a8be | ||
|
|
31dce0a9f5 | ||
|
|
46a83d7c7c | ||
|
|
f0ecda40e8 | ||
|
|
3c92005a3d | ||
|
|
9c6a3a4e39 | ||
|
|
71cd699488 | ||
|
|
9480a51059 | ||
|
|
dd7a803116 | ||
|
|
5f5cd6ac59 | ||
|
|
5552317fb8 | ||
|
|
4aebb52523 | ||
|
|
d22722bc33 | ||
|
|
7f432f453f | ||
|
|
44b2b0a9be | ||
|
|
30789e939e | ||
|
|
60a12f6710 | ||
|
|
838f628ddd | ||
|
|
294a3df7b4 | ||
|
|
6bfcbbb663 | ||
|
|
50e026d311 | ||
|
|
4dcb0b71be | ||
|
|
8bb527f04c | ||
|
|
1e7db2c92f | ||
|
|
4d761dd293 | ||
|
|
a40546606f | ||
|
|
fbd7fcb4f8 | ||
|
|
a59ce7a7ee | ||
|
|
e850bae696 | ||
|
|
c0cf273b7c | ||
|
|
cbb8faef12 | ||
|
|
d24b82b5c2 | ||
|
|
3f5ddf6a22 | ||
|
|
80f30bb29c | ||
|
|
c6e2b07fff | ||
|
|
80b0029d57 | ||
|
|
17bda59676 | ||
|
|
2fbcbaa525 | ||
|
|
22e47e4e43 | ||
|
|
1d4728e402 | ||
|
|
de4837f361 | ||
|
|
20d393e48e | ||
|
|
95714bdf5e | ||
|
|
8e2b593e3e | ||
|
|
885cf08a9d | ||
|
|
bd816e1124 | ||
|
|
98549f147c | ||
|
|
9de691c60e | ||
|
|
71e270dc50 | ||
|
|
26e684069d | ||
|
|
1c67df516e | ||
|
|
d7ed489876 | ||
|
|
267e487494 | ||
|
|
b07d830eb9 | ||
|
|
bfaaf09e57 | ||
|
|
33b2a569aa | ||
|
|
2febf6bcdd | ||
|
|
e2c9ee0e39 | ||
|
|
f017540571 | ||
|
|
5b65eb3d6e | ||
|
|
60874f31c9 | ||
|
|
78beb3c0b3 | ||
|
|
03633d94e5 | ||
|
|
392e4487b7 | ||
|
|
f316cb0e0c | ||
|
|
968ce7d8bb | ||
|
|
5876a11568 | ||
|
|
7448e1f4e4 | ||
|
|
4338e71005 | ||
|
|
7323412095 | ||
|
|
c1d5a17e45 | ||
|
|
2771ad0829 | ||
|
|
32cc988072 | ||
|
|
0b3ce2c8e0 | ||
|
|
9c46ee143e | ||
|
|
31c17a4d72 | ||
|
|
ee3f265294 | ||
|
|
c1cca91e68 | ||
|
|
038bafe20b | ||
|
|
f2fef264d7 | ||
|
|
fae77b4455 | ||
|
|
a3ffa0411e | ||
|
|
27dd680cbd | ||
|
|
2b5a370f48 | ||
|
|
21933f5a6a | ||
|
|
a5bd4f0c97 | ||
|
|
8e10aacf59 | ||
|
|
3dd74ef406 | ||
|
|
87f7085fc4 | ||
|
|
0b0b5a7c72 | ||
|
|
d6dee79bcf | ||
|
|
db05eeeed3 | ||
|
|
cc317a6572 | ||
|
|
149073466d | ||
|
|
776aec445b | ||
|
|
d6432b6455 | ||
|
|
81f3a0e300 | ||
|
|
167766519e | ||
|
|
b0d7156691 | ||
|
|
0fdf722a48 | ||
|
|
0382e6bc18 | ||
|
|
07a72ea5cb | ||
|
|
1d3647db98 | ||
|
|
53b96d399f | ||
|
|
78067fc8a6 | ||
|
|
4b29da8f5b | ||
|
|
74d6b37b00 | ||
|
|
d8049d8b83 |
No files matched your search
@@ -1,16 +0,0 @@
|
||||
{
|
||||
"presets": [
|
||||
["env", {
|
||||
"targets": {
|
||||
"node": "8.9"
|
||||
}
|
||||
}]
|
||||
],
|
||||
"plugins": [
|
||||
["module-resolver", {
|
||||
"alias": {
|
||||
"turtle": "./build"
|
||||
}
|
||||
}]
|
||||
]
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
version: 2
|
||||
|
||||
jobs:
|
||||
build:
|
||||
docker:
|
||||
- image: circleci/node:8.12
|
||||
steps:
|
||||
- checkout
|
||||
- run: yarn
|
||||
- run: yarn lint
|
||||
- run: yarn build
|
||||
test:
|
||||
docker:
|
||||
- image: circleci/node:8.12
|
||||
steps:
|
||||
- checkout
|
||||
- run: yarn
|
||||
- run: ./test-ci-unit.sh
|
||||
|
||||
workflows:
|
||||
version: 2
|
||||
build_and_test:
|
||||
jobs:
|
||||
- build
|
||||
- test
|
||||
@@ -1,18 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
source_local() {
|
||||
file=./.envrc.local
|
||||
if [[ -f "$file" ]]; then
|
||||
source_env $file
|
||||
fi
|
||||
watch_file $file
|
||||
}
|
||||
|
||||
source_up || true
|
||||
source_local
|
||||
|
||||
export NODE_ENV=development
|
||||
|
||||
export EXPO_TURTLE_TOOLS_DIR="$(pwd)/tools"
|
||||
|
||||
PATH_add $EXPO_TURTLE_TOOLS_DIR/bin
|
||||
@@ -1,11 +0,0 @@
|
||||
# Why
|
||||
|
||||
Please describe the motivation for this PR, and link to relevant GitHub issues, forums posts, or feature requests.
|
||||
|
||||
# How
|
||||
|
||||
How did you build this feature or fix this bug and why?
|
||||
|
||||
# Test Plan
|
||||
|
||||
Please describe how you tested this change and how a reviewer could reproduce your test, especially if this PR does not include automated tests! If possible, please also provide terminal output and/or screenshots demonstrating your test/reproduction.
|
||||
@@ -1,8 +0,0 @@
|
||||
!/src/node_modules
|
||||
|
||||
/build/
|
||||
/workingdir/*
|
||||
/artifacts/*
|
||||
shell.apk
|
||||
node_modules
|
||||
.secrets*
|
||||
@@ -1,9 +0,0 @@
|
||||
LOGGER_LEVEL="debug"
|
||||
API_PROTOCOL="http"
|
||||
API_HOSTNAME="localhost"
|
||||
API_PORT="3000"
|
||||
LOGGLY_TOKEN=""
|
||||
SENTRY_DSN=""
|
||||
ENVIRONMENT="development"
|
||||
REDIS_URL="redis://localhost:6379/0"
|
||||
REDIS_CONFIG_URL="redis://localhost:6379/0"
|
||||
@@ -1,25 +0,0 @@
|
||||
# Changelog
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
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.5.1] - 2019-01-16
|
||||
### Changed
|
||||
- Update Android shell app for SDK32 that includes react-native-screens@1.0.0-alpha.22
|
||||
|
||||
## [0.5.0] - 2019-01-16
|
||||
### Added
|
||||
- Turtle version is added to onFinish and onError build messages.
|
||||
### Removed
|
||||
- `--push-p12-path` parameter for `turtle build:ios` command,
|
||||
a Push Notifications Certificate is no longer required to perform a successful
|
||||
iOS standalone app build.
|
||||
- new/old Turtle version build argument
|
||||
|
||||
## [0.4.3] - 2019-01-09
|
||||
### Changed
|
||||
- Update iOS shell app for SDK32.
|
||||
- Update Android shell app for SDK32.
|
||||
@@ -1,7 +0,0 @@
|
||||
# Community Code of Conduct
|
||||
|
||||
The Expo team is committed to fostering an open and welcoming environment.
|
||||
|
||||
**Our Community Guidelines can be found here:**
|
||||
|
||||
https://expo.io/guidelines
|
||||
-124
@@ -1,124 +0,0 @@
|
||||
FROM openjdk:8u141
|
||||
|
||||
ENV DEBIAN_FRONTEND noninteractive
|
||||
|
||||
# https://github.com/yarnpkg/yarn/issues/2821
|
||||
RUN apt-get update && apt-get install apt-transport-https
|
||||
RUN curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add -
|
||||
RUN echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list
|
||||
|
||||
# Install dependencies
|
||||
RUN dpkg --add-architecture i386 && \
|
||||
apt-get update && \
|
||||
apt-get install -yq \
|
||||
ant\
|
||||
build-essential\
|
||||
bzip2:i386\
|
||||
file\
|
||||
lib32ncurses5\
|
||||
lib32z1\
|
||||
libc6:i386\
|
||||
libncurses5:i386\
|
||||
libstdc++6:i386\
|
||||
unzip\
|
||||
yarn\
|
||||
zlib1g:i386\
|
||||
--no-install-recommends && \
|
||||
apt-get clean
|
||||
|
||||
ENV NPM_CONFIG_LOGLEVEL warn
|
||||
ENV NODE_VERSION 8.12.0
|
||||
|
||||
RUN curl -SLO "https://nodejs.org/dist/v$NODE_VERSION/node-v$NODE_VERSION-linux-x64.tar.xz" \
|
||||
&& tar -xJf "node-v$NODE_VERSION-linux-x64.tar.xz" -C /usr/local --strip-components=1 \
|
||||
&& ln -s /usr/local/bin/node /usr/local/bin/nodejs
|
||||
|
||||
# Download and untar SDK
|
||||
ENV ANDROID_HOME /usr/local/android-sdk-linux
|
||||
ENV ANDROID_SDK /usr/local/android-sdk-linux
|
||||
ENV ANDROID_SDK_URL https://dl.google.com/android/repository/sdk-tools-linux-3859397.zip
|
||||
RUN mkdir -p ${ANDROID_HOME} && \
|
||||
curl -L "${ANDROID_SDK_URL}" > ${ANDROID_HOME}/sdk.zip && \
|
||||
cd ${ANDROID_HOME} && \
|
||||
unzip -qq sdk.zip && \
|
||||
rm sdk.zip && \
|
||||
# prevents warnings about missing repo config
|
||||
mkdir -p ${HOME}/.android && \
|
||||
touch ${HOME}/.android/repositories.cfg
|
||||
|
||||
ENV PATH ${ANDROID_HOME}/platform-tools:${PATH}
|
||||
ENV PATH ${ANDROID_HOME}/tools:${PATH}
|
||||
ENV PATH ${ANDROID_HOME}/tools/bin:${PATH}
|
||||
ENV PATH ${ANDROID_HOME}/build-tools/25.0.0/:${PATH}
|
||||
|
||||
RUN yes | sdkmanager --licenses > /dev/null
|
||||
|
||||
# Install Android SDK components
|
||||
RUN sdkmanager \
|
||||
"platform-tools" \
|
||||
"platforms;android-23" \
|
||||
"build-tools;25.0.0" \
|
||||
"extras;android;m2repository" \
|
||||
"extras;google;m2repository"
|
||||
|
||||
# Install Android NDK
|
||||
ENV ANDROID_NDK_VERSION android-ndk-r10e
|
||||
ENV ANDROID_NDK_FILE ${ANDROID_NDK_VERSION}-linux-x86_64.bin
|
||||
ENV ANDROID_NDK_URL https://dl.google.com/android/ndk/${ANDROID_NDK_FILE}
|
||||
ENV ANDROID_NDK /opt/${ANDROID_NDK_VERSION}
|
||||
ENV PATH $ANDROID_NDK:$PATH
|
||||
|
||||
# i have no clue why some of this is here, but it's working elsewhere
|
||||
# it appears to install the NDK version expo needs, so that's cool
|
||||
RUN mkdir /ndk_setup && cd /ndk_setup && \
|
||||
curl -L ${ANDROID_NDK_URL} > ${ANDROID_NDK_FILE} && \
|
||||
chmod +x ${ANDROID_NDK_FILE} && \
|
||||
sync && \
|
||||
./${ANDROID_NDK_FILE} && \
|
||||
mv ./${ANDROID_NDK_VERSION} ${ANDROID_NDK} && \
|
||||
cp -R \
|
||||
${ANDROID_NDK}/toolchains/arm-linux-androideabi-4.8/prebuilt/linux-x86_64 \
|
||||
${ANDROID_NDK}/toolchains/arm-linux-androideabi-4.8/prebuilt/linux-x86 && \
|
||||
cp -R \
|
||||
${ANDROID_NDK}/toolchains/x86-4.8/prebuilt/linux-x86_64 \
|
||||
${ANDROID_NDK}/toolchains/x86-4.8/prebuilt/linux-x86 && \
|
||||
cd && rm -rf /ndk_setup
|
||||
|
||||
# Support Gradle
|
||||
ENV TERM dumb
|
||||
|
||||
# Install gradle
|
||||
RUN wget https://services.gradle.org/distributions/gradle-4.4-all.zip
|
||||
RUN unzip -qq gradle-4.4-all.zip
|
||||
RUN mv gradle-4.4 /usr/local
|
||||
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
|
||||
|
||||
# Install Git
|
||||
RUN echo 'deb http://http.debian.net/debian wheezy-backports main' >> /etc/apt/sources.list && \
|
||||
apt-get update && \
|
||||
apt-get -t wheezy-backports install git-core && \
|
||||
apt-get clean && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
ADD . /app
|
||||
|
||||
RUN for SDK_VERSION in `ls /app/workingdir/android/`; do \
|
||||
cd /app/workingdir/android/$SDK_VERSION && \
|
||||
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
|
||||
|
||||
ENV NODE_ENV "production"
|
||||
ENV TURTLE_WORKING_DIR_PATH /app/workingdir/
|
||||
|
||||
CMD ["node", "./build/server.js"]
|
||||
@@ -1,21 +0,0 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-present 650 Industries, Inc. (aka Expo)
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -1,25 +1 @@
|
||||
# Turtle CLI
|
||||
|
||||
[](https://github.com/expo/turtle/blob/master/LICENSE)
|
||||
[](https://rubygems.org/gems/fastlane)
|
||||
[](https://circleci.com/gh/expo/turtle)
|
||||
|
||||
Turtle CLI is a command line interface for building Expo standalone apps.
|
||||
You can use it both on your CI and your private computer.
|
||||
|
||||
### Glossary
|
||||
|
||||
- **Turtle** - script which takes a shell app, fills it with user's code and produces a standalone app
|
||||
- **Shell app** — precompiled (in case of iOS builds) Expo app which doesn't have user's code
|
||||
- **Standalone app** - .ipa/.apk file, a user uploads standalone apps to the Google Play Store/Apple App Store
|
||||
|
||||
## Installation
|
||||
|
||||
[Installation instructions and documentation here.](https://docs.expo.io/versions/latest/distribution/turtle-cli)
|
||||
|
||||
## Usage example
|
||||
See the [expo/turtle-cli-example](https://github.com/expo/turtle-cli-example) repository to learn how to use Turtle CLI in order to build Expo standalone apps on your CI.
|
||||
|
||||
## License
|
||||
The Turtle CLI source code is made available under the [MIT license](LICENSE).
|
||||
Some of the dependencies are licensed differently, with the BSD license, for example.
|
||||
this commit is not really deployed to our builders :(
|
||||
@@ -1,6 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
'use strict';
|
||||
|
||||
require('source-map-support').install();
|
||||
require('../build/bin/env').initOfflineEnv();
|
||||
require('../build/bin/index').run('turtle');
|
||||
-91
@@ -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'));
|
||||
@@ -1,14 +0,0 @@
|
||||
{
|
||||
"displayName": "turtle",
|
||||
"testEnvironment": "node",
|
||||
"rootDir": "../src",
|
||||
"moduleNameMapper": {
|
||||
"^turtle/(.*)$": "<rootDir>/$1"
|
||||
},
|
||||
"transform": {
|
||||
"^.+\\.ts$": "ts-jest"
|
||||
},
|
||||
"testRegex": "(test|spec)\\.ts$",
|
||||
"moduleFileExtensions": ["ts", "js"],
|
||||
"verbose": true
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
{
|
||||
"displayName": "turtle",
|
||||
"testEnvironment": "node",
|
||||
"rootDir": "../src",
|
||||
"moduleNameMapper": {
|
||||
"^turtle/(.*)$": "<rootDir>/$1"
|
||||
},
|
||||
"transform": {
|
||||
"^.+\\.ts$": "ts-jest"
|
||||
},
|
||||
"testMatch": [ "**/__smoke__/*.test.ts" ],
|
||||
"moduleFileExtensions": ["ts", "js"],
|
||||
"verbose": true
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
{
|
||||
"displayName": "turtle",
|
||||
"testEnvironment": "node",
|
||||
"rootDir": "../src",
|
||||
"moduleNameMapper": {
|
||||
"^turtle/(.*)$": "<rootDir>/$1"
|
||||
},
|
||||
"transform": {
|
||||
"^.+\\.ts$": "ts-jest"
|
||||
},
|
||||
"testMatch": [ "**/__tests__/*.ts" ],
|
||||
"moduleFileExtensions": ["ts", "js"],
|
||||
"verbose": true
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es2017",
|
||||
"allowSyntheticDefaultImports": false,
|
||||
"baseUrl": "./",
|
||||
"paths": {
|
||||
"turtle/*": ["src/*"],
|
||||
}
|
||||
},
|
||||
"exclude": ["node_modules", "dist", "build"]
|
||||
}
|
||||
@@ -1,153 +0,0 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: turtle-android
|
||||
namespace: ${NAMESPACE}
|
||||
spec:
|
||||
replicas: ${REPLICAS}
|
||||
minReadySeconds: 5
|
||||
revisionHistoryLimit: 10
|
||||
strategy:
|
||||
type: RollingUpdate
|
||||
rollingUpdate:
|
||||
maxUnavailable: 5
|
||||
selector:
|
||||
matchLabels:
|
||||
app: turtle-android
|
||||
environment: ${NAMESPACE}
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: turtle-android
|
||||
environment: ${NAMESPACE}
|
||||
spec:
|
||||
restartPolicy: Always
|
||||
terminationGracePeriodSeconds: 1800 ## wait 30 minutes for any builds to complete
|
||||
tolerations:
|
||||
- key: dedicated
|
||||
operator: Equal
|
||||
value: jobs
|
||||
effect: NoSchedule
|
||||
affinity:
|
||||
nodeAffinity:
|
||||
requiredDuringSchedulingIgnoredDuringExecution:
|
||||
nodeSelectorTerms:
|
||||
- matchExpressions:
|
||||
- key: dedicated
|
||||
operator: In
|
||||
values:
|
||||
- jobs
|
||||
containers:
|
||||
- name: turtle-android
|
||||
image: ${IMAGE}
|
||||
env:
|
||||
# static vars
|
||||
- name: ENVIRONMENT
|
||||
value: ${NAMESPACE}
|
||||
- name: PLATFORM
|
||||
value: android
|
||||
- name: AWS_S3_BUCKET
|
||||
value: exp-shell-app-assets
|
||||
- name: AWS_S3_REGION
|
||||
value: us-west-1
|
||||
- name: LOGGLY_SUBDOMAIN
|
||||
value: exponent
|
||||
- name: TURTLE_TEMPORARY_FILES_DIR
|
||||
value: /tmp/turtle
|
||||
# vars from k8s secrets
|
||||
- name: AWS_ACCESS_KEY_ID
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: turtle-js-config
|
||||
key: aws_access_key_id
|
||||
- name: AWS_SECRET_ACCESS_KEY
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: turtle-js-config
|
||||
key: aws_secret_access_key
|
||||
- name: AWS_SQS_REGION
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: turtle-js-config
|
||||
key: aws_sqs_region
|
||||
- name: AWS_SQS_IOS_QUEUE_URL
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: turtle-js-config
|
||||
key: aws_sqs_ios_queue_url
|
||||
- name: AWS_SQS_ANDROID_QUEUE_URL
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: turtle-js-config
|
||||
key: aws_sqs_android_queue_url
|
||||
- name: AWS_SQS_IOS_PRIORITY_QUEUE_URL
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: turtle-js-config
|
||||
key: aws_sqs_ios_priority_queue_url
|
||||
- name: AWS_SQS_ANDROID_PRIORITY_QUEUE_URL
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: turtle-js-config
|
||||
key: aws_sqs_android_priority_queue_url
|
||||
- name: AWS_SQS_OUT_QUEUE_URL
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: turtle-js-config
|
||||
key: aws_sqs_out_queue_url
|
||||
- name: AWS_CLOUDWATCH_REGION
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: turtle-js-config
|
||||
key: aws_cloudwatch_region
|
||||
- name: LOGGLY_TOKEN
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: turtle-js-config
|
||||
key: loggly_token
|
||||
- name: REDIS_URL
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: turtle-js-config
|
||||
key: redis_url
|
||||
- name: REDIS_CONFIG_URL
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: turtle-js-config
|
||||
key: turtle_redis_url
|
||||
- name: SENTRY_DSN
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: turtle-js-config
|
||||
key: sentry_dsn
|
||||
- name: API_PROTOCOL
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: turtle-js-config
|
||||
key: api_protocol
|
||||
- name: API_HOSTNAME
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: turtle-js-config
|
||||
key: api_hostname
|
||||
- name: API_PORT
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: turtle-js-config
|
||||
key: api_port
|
||||
# other vars
|
||||
- name: GRADLE_DAEMON_DISABLED
|
||||
value: "1"
|
||||
- name: DISABLE_DEX_MAX_HEAP
|
||||
value: "true"
|
||||
- name: JAVA_OPTS
|
||||
value: '-Xms2048m -XX:+HeapDumpOnOutOfMemoryError -XX:+UnlockExperimentalVMOptions -XX:+UseCGroupMemoryLimitForHeap -XX:+UseG1GC'
|
||||
- name: GRADLE_OPTS
|
||||
value: '-Dorg.gradle.parallel=true -Dorg.gradle.configureondemand=true -Dorg.gradle.daemon=false -Dorg.gradle.jvmargs="-Xms2048m -XX:+HeapDumpOnOutOfMemoryError -XX:+UnlockExperimentalVMOptions -XX:+UseCGroupMemoryLimitForHeap -XX:+UseG1GC"'
|
||||
resources:
|
||||
requests:
|
||||
memory: "8192Mi"
|
||||
cpu: "2000m"
|
||||
limits:
|
||||
memory: "8192Mi"
|
||||
cpu: "2000m"
|
||||
-123
@@ -1,123 +0,0 @@
|
||||
{
|
||||
"name": "turtle-cli",
|
||||
"version": "0.5.1",
|
||||
"description": "The command-line tool for building standalone Expo apps",
|
||||
"preferGlobal": true,
|
||||
"scripts": {
|
||||
"start": "EXPENV_OVERRIDES=local-common,local expenv gulp",
|
||||
"start:ios": "PLATFORM=ios yarn start",
|
||||
"start:android": "PLATFORM=android yarn start",
|
||||
"watch": "EXPENV_OVERRIDES=local-common,local expenv gulp watch:source",
|
||||
"bin:turtle": "./bin/turtle.js",
|
||||
"lint": "tslint -c tslint.json 'src/**/*.ts'",
|
||||
"prepare": "yarn build",
|
||||
"build": "gulp build:server",
|
||||
"build:production": "./scripts/build.sh",
|
||||
"build:android-tarball:local": "./scripts/android/buildAndroidTarballLocally.sh",
|
||||
"init-workingdir:local": "./scripts/initLocalWorkingdir.sh",
|
||||
"init-workingdir:remote": "yarn init-workingdir:remote-android && yarn init-workingdir:remote-ios",
|
||||
"init-workingdir:remote-android": "./scripts/android/fetchRemoteAndroidTarball.sh",
|
||||
"init-workingdir:remote-ios": "./scripts/ios/fetchRemoteIosTarball.sh",
|
||||
"docker:pull": "retry5 gcloud docker -- pull gcr.io/exponentjs/turtle-android:latest",
|
||||
"docker:pull:tag": "retry5 gcloud docker -- pull gcr.io/exponentjs/turtle-android:${TAG}",
|
||||
"docker:build": "yarn build:production android && gcloud docker -- build -t gcr.io/exponentjs/turtle-android:latest -t gcr.io/exponentjs/turtle-android:${TAG} .",
|
||||
"docker:push": "retry5 gcloud docker -- push gcr.io/exponentjs/turtle-android:${TAG} && retry5 gcloud docker -- push gcr.io/exponentjs/turtle-android:latest",
|
||||
"deploy:android": "./scripts/android/deploy.sh",
|
||||
"test": "yarn jest",
|
||||
"test:integration": "yarn test --config jest/integration-config.json",
|
||||
"test:unit": "yarn test --config jest/unit-config.json",
|
||||
"test:smoke": "yarn test --config jest/smoke-config.json --forceExit",
|
||||
"test:smoke:android": "yarn test:smoke --testNamePattern=android",
|
||||
"test:smoke:ios": "PLATFORM=ios yarn test:smoke --testNamePattern=iOS",
|
||||
"secrets:init-private": "./scripts/initPrivateSecrets.sh",
|
||||
"ssh:android:pods-names": "./scripts/ssh/findAndroidPods.sh",
|
||||
"ssh:android:connect": "./scripts/ssh/connectToAndroidPod.sh",
|
||||
"ssh:ios:ips": "./scripts/ssh/findIosIps.sh",
|
||||
"set-priorities": "yarn set-priorities:local",
|
||||
"set-priorities:local": "EXPENV_OVERRIDES=local-common,local 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"
|
||||
},
|
||||
"bin": {
|
||||
"turtle": "./bin/turtle.js"
|
||||
},
|
||||
"files": [
|
||||
"bin",
|
||||
"build",
|
||||
"scripts/cli",
|
||||
"shellTarballs"
|
||||
],
|
||||
"keywords": [
|
||||
"expo",
|
||||
"react-native"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
},
|
||||
"author": "Expo <[email protected]>",
|
||||
"license": "MIT",
|
||||
"bugs": {
|
||||
"url": "https://github.com/expo/turtle/issues"
|
||||
},
|
||||
"homepage": "https://github.com/expo/turtle#readme",
|
||||
"devDependencies": {
|
||||
"@types/decompress": "^4.2.3",
|
||||
"@types/fs-extra": "^5.0.4",
|
||||
"@types/jest": "^23.3.9",
|
||||
"@types/joi": "^13.4.3",
|
||||
"@types/lodash": "^4.14.109",
|
||||
"@types/progress": "^2.0.1",
|
||||
"@types/raven": "^2.5.1",
|
||||
"@types/request": "^2.47.1",
|
||||
"@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",
|
||||
"jest": "^23.6.0",
|
||||
"nock": "^10.0.1",
|
||||
"nodemon": "^1.18.7",
|
||||
"rimraf": "^2.6.2",
|
||||
"source-map-loader": "^0.2.3",
|
||||
"ts-jest": "^23.10.4",
|
||||
"tslint": "^5.10.0",
|
||||
"typescript": "^3.0.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"async-retry": "^1.2.1",
|
||||
"aws-sdk": "^2.226.1",
|
||||
"bunyan": "^1.8.12",
|
||||
"bunyan-debug-stream": "^1.0.8",
|
||||
"bunyan-loggly": "^1.3.4",
|
||||
"bunyan-sentry-stream": "^1.2.1",
|
||||
"commander": "^2.15.1",
|
||||
"copy": "^0.3.1",
|
||||
"decompress": "^4.2.0",
|
||||
"fs-extra": "^6.0.0",
|
||||
"ioredis": "^3.2.2",
|
||||
"is-semver": "^1.0.8",
|
||||
"joi": "^13.2.0",
|
||||
"lodash": "^4.17.10",
|
||||
"progress": "^2.0.0",
|
||||
"raven": "^2.6.2",
|
||||
"request": "^2.88.0",
|
||||
"request-progress": "^3.0.0",
|
||||
"sharp": "^0.21.0",
|
||||
"source-map-support": "^0.5.6",
|
||||
"tar": "^4.4.6",
|
||||
"uuid": "^3.2.1",
|
||||
"which": "^1.3.1",
|
||||
"xdl": "52.0.14-alpha.6+371be041"
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -1,10 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -xeuo pipefail
|
||||
|
||||
environment=$1
|
||||
|
||||
retry5 gcloud container images add-tag "${IMAGE}:${PREV_TAG}" "${IMAGE}:${CURRENT_TAG}"
|
||||
export TAG="${CURRENT_TAG}"
|
||||
|
||||
nix run expo.k8s-services.turtle.${environment}.deploy --command deploy
|
||||
@@ -1,18 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -xeo pipefail
|
||||
|
||||
ROOT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )"/../.. && pwd )"
|
||||
ARTIFACTS_DIR="$ROOT_DIR/artifacts"
|
||||
WORKING_DIR="$ROOT_DIR/workingdir/android"
|
||||
ANDROID_SHELL_TARBALLS_DIRECTORY="$ROOT_DIR/shellTarballs/android"
|
||||
|
||||
mkdir -p $ARTIFACTS_DIR
|
||||
|
||||
for SDK_VERSION in `ls $ANDROID_SHELL_TARBALLS_DIRECTORY`; do
|
||||
mkdir -p $WORKING_DIR/$SDK_VERSION
|
||||
TARBALL_URI=$(head -n 1 $ANDROID_SHELL_TARBALLS_DIRECTORY/$SDK_VERSION)
|
||||
run expo.s3env --keep AWS_ACCESS_KEY_ID --keep AWS_SECRET_ACCESS_KEY --command retry5 aws s3 cp $TARBALL_URI $ARTIFACTS_DIR/$SDK_VERSION.tar.gz --no-progress
|
||||
tar zxf $ARTIFACTS_DIR/$SDK_VERSION.tar.gz -C $WORKING_DIR/$SDK_VERSION
|
||||
rm -rf $TARBALL_URI $ARTIFACTS_DIR/$SDK_VERSION.tar.gz
|
||||
done
|
||||
@@ -1,28 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -xeo pipefail
|
||||
|
||||
if [ -z "$1" ]; then
|
||||
echo "Please specify a platform to build"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
PLATFORM=$1
|
||||
|
||||
yarn
|
||||
yarn run build
|
||||
yarn init-workingdir:remote-$PLATFORM
|
||||
|
||||
if [[ -z "${TARBALL}" ]]; then
|
||||
echo "TARBALL is not set, building tarball skipped"
|
||||
else
|
||||
tar zcf "$TARBALL" \
|
||||
workingdir \
|
||||
build \
|
||||
scripts \
|
||||
package.json \
|
||||
node_modules \
|
||||
yarn.lock \
|
||||
tsconfig.json \
|
||||
gulpfile.js
|
||||
fi
|
||||
@@ -1,9 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
yes | $ANDROID_HOME/tools/bin/sdkmanager --licenses > /dev/null
|
||||
$ANDROID_HOME/tools/bin/sdkmanager \
|
||||
"platform-tools" \
|
||||
"platforms;android-23" \
|
||||
"build-tools;25.0.0" \
|
||||
"extras;android;m2repository" \
|
||||
"extras;google;m2repository"
|
||||
@@ -1,22 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
ensure_link_exists() {
|
||||
if [ ! -L "$2" ]; then
|
||||
ln -s $1 $2
|
||||
echo "$2 link created"
|
||||
fi
|
||||
}
|
||||
|
||||
ROOT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )"/.. && pwd )"
|
||||
WORKINGDIR="$ROOT_DIR/workingdir/local"
|
||||
|
||||
if [ ! -d "$WORKINGDIR" ]; then
|
||||
mkdir -p $WORKINGDIR
|
||||
echo "$WORKINGDIR directory created"
|
||||
fi
|
||||
|
||||
LINKS=( "template-files" "package.json" "android" "expokit-npm-package" "packages" "client-builds" "cpp" "exponent-view-template" "ios" "shellAppBase-builds" "shellAppWorkspaces")
|
||||
|
||||
for LINKNAME in ${LINKS[@]}; do
|
||||
ensure_link_exists "$EXPO_UNIVERSE_DIR/exponent/$LINKNAME" "$WORKINGDIR/$LINKNAME"
|
||||
done
|
||||
@@ -1,23 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
if [ -z "$1" ]; then
|
||||
echo "Please specify a nickname you used to create AWS SQS queues"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
TURTLE_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )"/.. && pwd )"
|
||||
TERRAFORM_DIR="${EXPO_UNIVERSE_DIR}/terraform"
|
||||
|
||||
ANDROID=`cd $TERRAFORM_DIR; terraform state show module.turtle_queues_local_$1.aws_sqs_queue.jobs_android_in | head -1 | cut -d"=" -f2 | xargs`
|
||||
IOS=`cd $TERRAFORM_DIR; terraform state show module.turtle_queues_local_$1.aws_sqs_queue.jobs_ios_in | head -1 | cut -d"=" -f2 | xargs`
|
||||
OUT=`cd $TERRAFORM_DIR; terraform state show module.turtle_queues_local_$1.aws_sqs_queue.jobs_out | head -1 | cut -d"=" -f2 | xargs`
|
||||
|
||||
cat << EOF > $TURTLE_DIR/.secrets.local
|
||||
AWS_SQS_ANDROID_QUEUE_URL="$ANDROID"
|
||||
AWS_SQS_IOS_QUEUE_URL="$IOS"
|
||||
AWS_SQS_OUT_QUEUE_URL="$OUT"
|
||||
TURTLE_FAKE_UPLOAD="1"
|
||||
TURTLE_FAKE_UPLOAD_DIR="/Users/$USER/Downloads"
|
||||
TURTLE_WORKING_DIR_PATH="$EXPO_UNIVERSE_DIR/server/turtle/workingdir"
|
||||
TURTLE_USE_LOCAL_WORKING_DIR="1"
|
||||
EOF
|
||||
@@ -1,18 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -xeo pipefail
|
||||
|
||||
ROOT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )"/../.. && pwd )"
|
||||
ARTIFACTS_DIR="$ROOT_DIR/artifacts"
|
||||
WORKING_DIR="$ROOT_DIR/workingdir/ios"
|
||||
IOS_SHELL_TARBALLS_DIRECTORY="$ROOT_DIR/shellTarballs/ios"
|
||||
|
||||
mkdir -p $ARTIFACTS_DIR
|
||||
|
||||
for SDK_VERSION in `ls $IOS_SHELL_TARBALLS_DIRECTORY`; do
|
||||
mkdir -p $WORKING_DIR/$SDK_VERSION
|
||||
TARBALL_URI=$(head -n 1 $IOS_SHELL_TARBALLS_DIRECTORY/$SDK_VERSION)
|
||||
run expo.s3env --keep AWS_ACCESS_KEY_ID --keep AWS_SECRET_ACCESS_KEY --command retry5 aws s3 cp $TARBALL_URI $ARTIFACTS_DIR/$SDK_VERSION.tar.gz --no-progress
|
||||
tar zxf $ARTIFACTS_DIR/$SDK_VERSION.tar.gz -C $WORKING_DIR/$SDK_VERSION
|
||||
rm -rf $TARBALL_URI $ARTIFACTS_DIR/$SDK_VERSION.tar.gz
|
||||
done
|
||||
@@ -1,73 +0,0 @@
|
||||
import { env } from 'turtle/utils/env';
|
||||
import { connect, RedisClient } from 'turtle/utils/redis';
|
||||
import { PLATFORMS } from 'turtle/constants';
|
||||
import {
|
||||
createConfigurationsKey,
|
||||
createDefaultConfigurationKey,
|
||||
HIGH_CONFIGURATION,
|
||||
NORMAL_CONFIGURATION,
|
||||
} from 'turtle/utils/priorities';
|
||||
|
||||
const TIMEOUT = 3000;
|
||||
|
||||
// Turtle before doing anything (and after every build) checks for the first config
|
||||
// (an element in config table with key=i) that is not taken (config.i is null/outdated)
|
||||
// or is taken by this process itself.
|
||||
// If Turtle dies unexpectedly the configuration will become free after TTL expires,
|
||||
// so the state (number of HP/NP turtles) should come back to normal soon.
|
||||
|
||||
// For example, if a configuration table looks like this:
|
||||
// [ HIGH_CONFIGURATION, NORMAL_CONFIGURATION, HIGH_CONFIGURATION ]
|
||||
// If there is one turtle it runs with High Priority first.
|
||||
// If there are two turtles, one runs with High Priority first, the second one with Normal Priority first.
|
||||
// If the first dies, and the third is started in his place, it gets 3rd config (again HP-first),
|
||||
// but after some time, because of TTL for config reservations,
|
||||
// they should come back to using configurations no. 1 and 2.
|
||||
// If there are no more available configurations from the table, the default configuration is used.
|
||||
|
||||
|
||||
const AVAILABLE_CONFIGURATIONS_IOS = [
|
||||
HIGH_CONFIGURATION,
|
||||
HIGH_CONFIGURATION,
|
||||
NORMAL_CONFIGURATION,
|
||||
NORMAL_CONFIGURATION,
|
||||
HIGH_CONFIGURATION,
|
||||
NORMAL_CONFIGURATION,
|
||||
];
|
||||
|
||||
const AVAILABLE_CONFIGURATIONS_ANDROID = [
|
||||
HIGH_CONFIGURATION,
|
||||
HIGH_CONFIGURATION,
|
||||
NORMAL_CONFIGURATION,
|
||||
NORMAL_CONFIGURATION,
|
||||
HIGH_CONFIGURATION,
|
||||
HIGH_CONFIGURATION,
|
||||
NORMAL_CONFIGURATION,
|
||||
NORMAL_CONFIGURATION,
|
||||
HIGH_CONFIGURATION,
|
||||
HIGH_CONFIGURATION,
|
||||
NORMAL_CONFIGURATION,
|
||||
NORMAL_CONFIGURATION,
|
||||
HIGH_CONFIGURATION,
|
||||
HIGH_CONFIGURATION,
|
||||
NORMAL_CONFIGURATION,
|
||||
NORMAL_CONFIGURATION,
|
||||
];
|
||||
|
||||
|
||||
const DEFAULT_CONFIGURATION = HIGH_CONFIGURATION;
|
||||
|
||||
async function run() {
|
||||
const redis = await connect(TIMEOUT, RedisClient.Configuration);
|
||||
|
||||
await redis.set(createConfigurationsKey(PLATFORMS.IOS), JSON.stringify(AVAILABLE_CONFIGURATIONS_IOS));
|
||||
await redis.set(createConfigurationsKey(PLATFORMS.ANDROID), JSON.stringify(AVAILABLE_CONFIGURATIONS_ANDROID));
|
||||
await redis.set(createDefaultConfigurationKey(PLATFORMS.IOS), JSON.stringify(DEFAULT_CONFIGURATION));
|
||||
await redis.set(createDefaultConfigurationKey(PLATFORMS.ANDROID), JSON.stringify(DEFAULT_CONFIGURATION));
|
||||
|
||||
redis.disconnect();
|
||||
}
|
||||
|
||||
run()
|
||||
.then(() => console.log('All done'))
|
||||
.catch(err => console.error('Error:', err));
|
||||
@@ -1,13 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
if [ -z "$1" ]; then
|
||||
echo "Please provide an environment name as a first argument"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -z "$2" ]; then
|
||||
echo "Please provide a pod name as a second argument"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
kubectl --namespace $1 exec -it $2 -- /bin/bash
|
||||
@@ -1,5 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
ENVIRONMENT=${1:-production}
|
||||
|
||||
kubectl get pods --namespace $ENVIRONMENT | grep "turtle-android-" | sed -e "s/ */ /g" | cut -d" " -f1
|
||||
@@ -1,5 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
ENVIRONMENT=${1:-production}
|
||||
|
||||
govc vm.ip -wait 5s turtle-js-"${ENVIRONMENT}"-*
|
||||
@@ -1 +0,0 @@
|
||||
s3://exp-artifacts/android-shell-builder-67bd02667e29221707f8c1dd11e739eada25ef55.tar.gz
|
||||
@@ -1 +0,0 @@
|
||||
s3://exp-artifacts/android-shell-builder-67bd02667e29221707f8c1dd11e739eada25ef55.tar.gz
|
||||
@@ -1 +0,0 @@
|
||||
s3://exp-artifacts/android-shell-builder-67bd02667e29221707f8c1dd11e739eada25ef55.tar.gz
|
||||
@@ -1 +0,0 @@
|
||||
s3://exp-artifacts/android-shell-builder-67bd02667e29221707f8c1dd11e739eada25ef55.tar.gz
|
||||
@@ -1 +0,0 @@
|
||||
s3://exp-artifacts/android-shell-builder-67bd02667e29221707f8c1dd11e739eada25ef55.tar.gz
|
||||
@@ -1 +0,0 @@
|
||||
s3://exp-artifacts/android-shell-builder-5c339ff7bab6f2161d378cc70e1bf04d32ad98dd.tar.gz
|
||||
@@ -1 +0,0 @@
|
||||
s3://exp-artifacts/android-shell-builder-cb03fdc12dc74f699a6d3f22219cbcd30e01ac46.tar.gz
|
||||
@@ -1 +0,0 @@
|
||||
s3://exp-artifacts/ios-shell-builder-sdk-26-db356a09877642d64023af98e4e2e1e632fbe4df.tar.gz
|
||||
@@ -1 +0,0 @@
|
||||
s3://exp-artifacts/ios-shell-builder-sdk-27-db356a09877642d64023af98e4e2e1e632fbe4df.tar.gz
|
||||
@@ -1 +0,0 @@
|
||||
s3://exp-artifacts/ios-shell-builder-sdk-28-db356a09877642d64023af98e4e2e1e632fbe4df.tar.gz
|
||||
@@ -1 +0,0 @@
|
||||
s3://exp-artifacts/ios-shell-builder-sdk-29-db356a09877642d64023af98e4e2e1e632fbe4df.tar.gz
|
||||
@@ -1 +0,0 @@
|
||||
s3://exp-artifacts/ios-shell-builder-sdk-30-7d017a96d00fbf18ee7e5581ea47528b9cc78afd.tar.gz
|
||||
@@ -1 +0,0 @@
|
||||
s3://exp-artifacts/ios-shell-builder-sdk-latest-56824ecea6ba10f5a9d8991545a1c47ee3284f5f.tar.gz
|
||||
@@ -1 +0,0 @@
|
||||
s3://exp-artifacts/ios-shell-builder-sdk-latest-e3855783ed0618f4dcf326dc353c6e2951e05c49.tar.gz
|
||||
File diff suppressed because it is too large.
Load diff
@@ -1,2 +0,0 @@
|
||||
recording.json filter=git-crypt diff=git-crypt
|
||||
.gitatrributes !filter !diff
|
||||
Binary file not shown.
@@ -1,106 +0,0 @@
|
||||
import fs from 'fs-extra';
|
||||
|
||||
import nock from 'nock';
|
||||
|
||||
import config from 'turtle/config';
|
||||
import { PLATFORMS } from 'turtle/constants';
|
||||
import * as jobManager from 'turtle/jobManager';
|
||||
import logger from 'turtle/logger';
|
||||
|
||||
jest.mock('../builders/utils/uploader.ts', () => ({
|
||||
uploadBuildToS3: jest.fn(() => {
|
||||
logger.debug('Faking uploading build');
|
||||
return 'legit_path';
|
||||
}),
|
||||
}));
|
||||
|
||||
async function performSmokeTest(recording: string) {
|
||||
jest.setTimeout(60 * 60 * 1000);
|
||||
|
||||
// @ts-ignore
|
||||
const fixtureExists = await fs.exists(recording);
|
||||
if (fixtureExists) {
|
||||
logger.info('Fixture exists, test will use existing one.');
|
||||
|
||||
nock.load(recording);
|
||||
mockLogs();
|
||||
mockSQS();
|
||||
logger.info('Loading fixture');
|
||||
} else {
|
||||
nock.recorder.rec({
|
||||
dont_print: true,
|
||||
output_objects: true,
|
||||
enable_reqheaders_recording: false,
|
||||
});
|
||||
}
|
||||
|
||||
await jobManager.doJob();
|
||||
if (!fixtureExists) {
|
||||
await fs.writeFile(recording, JSON.stringify(nock.recorder.play()));
|
||||
}
|
||||
}
|
||||
|
||||
function mockLogs() {
|
||||
/*
|
||||
Match log push messages. Seperate matcher, because it needs `persist` option
|
||||
due to fact that it's being called variable amount times.
|
||||
*/
|
||||
return nock('https://exp-shell-app-assets.s3.us-west-1.amazonaws.com')
|
||||
.persist()
|
||||
.put((uri: string) => {
|
||||
return uri.indexOf('/logs') >= 0;
|
||||
})
|
||||
.reply(200);
|
||||
}
|
||||
|
||||
function mockSQS() {
|
||||
return nock('https://sqs.us-west-2.amazonaws.com')
|
||||
.persist()
|
||||
.post(() => true)
|
||||
.reply(200);
|
||||
}
|
||||
|
||||
function mockFinish() {
|
||||
// A separate matcher for the final request to ensure build was completed successfully
|
||||
|
||||
return nock('https://sqs.us-west-2.amazonaws.com:443')
|
||||
.post('/', (body: any) => {
|
||||
const keys = Object.keys(body);
|
||||
if (keys.includes('MessageBody')) {
|
||||
const mb = JSON.parse(body.MessageBody);
|
||||
if (mb.status === 'finished') {
|
||||
return true;
|
||||
} else if (mb.status === 'errored') {
|
||||
fail('Build ended with error');
|
||||
}
|
||||
}
|
||||
return false;
|
||||
})
|
||||
.reply(200);
|
||||
}
|
||||
|
||||
async function testPlatform(platform: string) {
|
||||
process.env.PLATFORM = platform;
|
||||
|
||||
const utils = require('turtle/aws/utils');
|
||||
utils.QUEUE_URL = jest.fn((priority) => {
|
||||
return (config.sqs.queues as any)[priority][platform];
|
||||
});
|
||||
|
||||
nock.cleanAll();
|
||||
nock.enableNetConnect();
|
||||
|
||||
const finishMock = mockFinish();
|
||||
const fixtureLocation = `src/__smoke__/fixtures/${platform}/recording.json`;
|
||||
await performSmokeTest(fixtureLocation);
|
||||
expect(finishMock.isDone());
|
||||
nock.disableNetConnect();
|
||||
}
|
||||
|
||||
test('Android builds', async () => {
|
||||
await testPlatform(PLATFORMS.ANDROID);
|
||||
});
|
||||
|
||||
test('iOS builds', async () => {
|
||||
await testPlatform(PLATFORMS.IOS);
|
||||
});
|
||||
Vendored
-1
@@ -1 +0,0 @@
|
||||
declare module 'nock';
|
||||
@@ -1,90 +0,0 @@
|
||||
{
|
||||
"id":"5980f894-d403-4909-a5e5-375eac188ff8",
|
||||
"experienceId":"2c8d6f90-d05e-11e8-b439-fbdd43a1c21e",
|
||||
"experienceName":"@admin/testApp",
|
||||
"platform":"android",
|
||||
"config":{
|
||||
"releaseChannel":"default"
|
||||
},
|
||||
"status":"sent-to-queue",
|
||||
"credentials":{
|
||||
"keystore":"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",
|
||||
"keyPassword":"9f47f3284e2a461eba7bc6cff302035d",
|
||||
"keystoreAlias":"QGFkbWluL3Rlc3RBcHA=",
|
||||
"keystorePassword":"aadb0c482eb340c0aee02b525460c576"
|
||||
},
|
||||
"manifest":{
|
||||
"android":{
|
||||
"package":"com.yourcompany.yourappname"
|
||||
},
|
||||
"bundledAssets":[
|
||||
"asset_744ce60078c17d86006dd0edabcd59a7.ttf",
|
||||
"asset_5221f35630f5b71e53c3196d3ff681a1.ttf",
|
||||
"asset_517bb9a13989451be6623299153f503f.ttf",
|
||||
"asset_b06871f281fee6b241d60582ae9369b9.ttf",
|
||||
"asset_e20945d7c929279ef7a6f1db184a4470.ttf",
|
||||
"asset_74c652671225d6ded874a648502e5f0a.ttf",
|
||||
"asset_1eccbc4c41d49fd81840aef3eaabe862.ttf",
|
||||
"asset_a37b0c01c0baf1888ca812cc0508f6e2.ttf",
|
||||
"asset_9e9dbe78c20054a6d71d9c10c939ba68.ttf",
|
||||
"asset_d2285965fe34b05465047401b8595dd0.ttf",
|
||||
"asset_5cdf883b18a5651a29a4d1ef276d2457.ttf",
|
||||
"asset_7d40544b395c5949f4646f5e150fe020.png",
|
||||
"asset_cdd04e13d4ec83ff0cd13ec8dabdc341.png",
|
||||
"asset_a132ecc4ba5c1517ff83c0fb321bc7fc.png",
|
||||
"asset_0ea69b5077e7c4696db85dbcba75b0e1.png",
|
||||
"asset_f5b790e2ac193b3d41015edb3551f9b8.png",
|
||||
"asset_755fce460f64aed4d3f3ac1a6249ceea.png",
|
||||
"asset_45a8ce76165dcaec1c5e39be15478e53.png",
|
||||
"asset_2d8deea13c41b52b04442d91fb31c2c5.png",
|
||||
"asset_5c36108361d0a750fd25533356f80245.png",
|
||||
"asset_af47141a24eb8e5751dfd034159479bd.png",
|
||||
"asset_ad107d1ca027259ca50ce0f432293e6c.png",
|
||||
"asset_a30cd76fa74ee5a0bab5aba37c101416.png",
|
||||
"asset_49a79d66bdea2debf1832bf4d7aca127.ttf",
|
||||
"asset_778ffc9fe8773a878e9c30a6304784de.png",
|
||||
"asset_376d6a4c7f622917c39feb23671ef71d.png",
|
||||
"asset_c79c3606a1cf168006ad3979763c7e0c.png",
|
||||
"asset_02bc1fa7c0313217bde2d65ccbff40c9.png",
|
||||
"asset_35ba0eaec5a4f5ed12ca16fabeae451d.png"
|
||||
],
|
||||
"description":"A very interesting project.",
|
||||
"icon":"./assets/images/icon.png",
|
||||
"iconUrl":"https://d1wp6m56sqw74a.cloudfront.net/~assets/fa6577fecc0a7838f15a254577639984",
|
||||
"ios":{
|
||||
"bundleIdentifier":"com.niech.to.zadziala",
|
||||
"supportsTablet":true
|
||||
},
|
||||
"locales":{
|
||||
|
||||
},
|
||||
"name":"testApp",
|
||||
"orientation":"portrait",
|
||||
"platforms":[
|
||||
"ios",
|
||||
"android"
|
||||
],
|
||||
"privacy":"public",
|
||||
"sdkVersion":"29.0.0",
|
||||
"slug":"testApp",
|
||||
"splash":{
|
||||
"backgroundColor":"#ffffff",
|
||||
"image":"./assets/images/splash.png",
|
||||
"imageUrl":"https://d1wp6m56sqw74a.cloudfront.net/~assets/43ec0dcbe5a156bf9e650bb8c15e7af6",
|
||||
"resizeMode":"contain"
|
||||
},
|
||||
"updates":{
|
||||
"fallbackToCacheTimeout":0
|
||||
},
|
||||
"version":"1.0.0",
|
||||
"id":"@admin/testApp",
|
||||
"revisionId":"1.0.0-r.8wVOKbq7RP",
|
||||
"publishedTime":"2018-10-15T09:39:12.773Z",
|
||||
"commitTime":"2018-10-15T09:39:12.824Z",
|
||||
"bundleUrl":"https://d1wp6m56sqw74a.cloudfront.net/%40admin%2FtestApp%2F1.0.0%2Ffa27a628bfe715851c97f32868948509-29.0.0-android.js",
|
||||
"releaseChannel":"default",
|
||||
"hostUri":"exp.host/@admin/testApp"
|
||||
},
|
||||
"sdkVersion":"29.0.0",
|
||||
"messageCreatedTimestamp":1539596354060
|
||||
}
|
||||
@@ -1,121 +0,0 @@
|
||||
{
|
||||
"id": "2d46d6c6-aedd-429c-b5f7-a3a0b96fc916",
|
||||
"experienceId": "a3987870-d11f-11e8-b439-fbdd43a1c21e",
|
||||
"experienceName": "@admin/testApp",
|
||||
"platform": "ios",
|
||||
"config": {
|
||||
"releaseChannel": "default",
|
||||
"bundleIdentifier": "com.niech.to.zadziala"
|
||||
},
|
||||
"status": "sent-to-queue",
|
||||
"credentials": {
|
||||
"appId": "342LFMU86M",
|
||||
"pushId": "4VU5PQBGHM",
|
||||
"teamId": "QL76XYH73P",
|
||||
"pushP12": "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\nHNZoKAQIbBakg4AhjA4=\n",
|
||||
"features": {
|
||||
"push": true,
|
||||
"inAppPurchase": true,
|
||||
"gameCenter": true,
|
||||
"passbook": false,
|
||||
"dataProtection": "",
|
||||
"homeKit": false,
|
||||
"cloudKitVersion": 2,
|
||||
"iCloud": false,
|
||||
"LPLF93JG7M": false,
|
||||
"IAD53UNK2F": false,
|
||||
"V66P55NK2I": false,
|
||||
"SKC3T5S89Y": false,
|
||||
"APG3427HIY": false,
|
||||
"HK421J6T7P": false,
|
||||
"WC421J6T7P": false
|
||||
},
|
||||
"teamName": "Alicja Warchał (Individual)",
|
||||
"pushPassword": "ddCtxuuRHJZVPtf8lv/n1g==",
|
||||
"enabledFeatures": [
|
||||
"gameCenter",
|
||||
"inAppPurchase",
|
||||
"push"
|
||||
],
|
||||
"enterpriseAccount": "false",
|
||||
"provisioningProfile": "MIIdwAYJKoZIhvcNAQcCoIIdsTCCHa0CAQExCzAJBgUrDgMCGgUAMIINVAYJ\nKoZIhvcNAQcBoIINRQSCDUE8P3htbCB2ZXJzaW9uPSIxLjAiIGVuY29kaW5n\nPSJVVEYtOCI/Pgo8IURPQ1RZUEUgcGxpc3QgUFVCTElDICItLy9BcHBsZS8v\nRFREIFBMSVNUIDEuMC8vRU4iICJodHRwOi8vd3d3LmFwcGxlLmNvbS9EVERz\nL1Byb3BlcnR5TGlzdC0xLjAuZHRkIj4KPHBsaXN0IHZlcnNpb249IjEuMCI+\nCjxkaWN0PgoJPGtleT5BcHBJRE5hbWU8L2tleT4KCTxzdHJpbmc+YWRtaW50\nZXN0QXBwIGM0YThkZjNmMTZkNzg1OWIxODc0YWFlZWUzY2ZhZWUwPC9zdHJp\nbmc+Cgk8a2V5PkFwcGxpY2F0aW9uSWRlbnRpZmllclByZWZpeDwva2V5PgoJ\nPGFycmF5PgoJPHN0cmluZz5RTDc2WFlINzNQPC9zdHJpbmc+Cgk8L2FycmF5\nPgoJPGtleT5DcmVhdGlvbkRhdGU8L2tleT4KCTxkYXRlPjIwMTgtMTAtMDhU\nMTQ6MzA6NTBaPC9kYXRlPgoJPGtleT5QbGF0Zm9ybTwva2V5PgoJPGFycmF5\nPgoJCTxzdHJpbmc+aU9TPC9zdHJpbmc+Cgk8L2FycmF5PgogICAgPGtleT5J\nc1hjb2RlTWFuYWdlZDwva2V5PgogICAgPGZhbHNlLz4KCTxrZXk+RGV2ZWxv\ncGVyQ2VydGlmaWNhdGVzPC9rZXk+Cgk8YXJyYXk+CgkJPGRhdGE+TUlJRm9U\nQ0NCSW1nQXdJQkFnSUlmTnFoZUpOZndFOHdEUVlKS29aSWh2Y05BUUVMQlFB\nd2daWXhDekFKQmdOVkJBWVRBbFZUTVJNd0VRWURWUVFLREFwQmNIQnNaU0JK\nYm1NdU1Td3dLZ1lEVlFRTERDTkJjSEJzWlNCWGIzSnNaSGRwWkdVZ1JHVjJa\nV3h2Y0dWeUlGSmxiR0YwYVc5dWN6RkVNRUlHQTFVRUF3dzdRWEJ3YkdVZ1Yy\nOXliR1IzYVdSbElFUmxkbVZzYjNCbGNpQlNaV3hoZEdsdmJuTWdRMlZ5ZEds\nbWFXTmhkR2x2YmlCQmRYUm9iM0pwZEhrd0hoY05NVGd4TURBNE1UUXlNRE01\nV2hjTk1Ua3hNREE0TVRReU1ETTVXakNCbERFYU1CZ0dDZ21TSm9tVDhpeGtB\nUUVNQ2xGTU56WllXVWczTTFBeE9qQTRCZ05WQkFNTU1XbFFhRzl1WlNCRWFY\nTjBjbWxpZFhScGIyNDZJRUZzYVdOcVlTQlhZWEpqYUdIRmdpQW9VVXczTmxo\nWlNEY3pVQ2t4RXpBUkJnTlZCQXNNQ2xGTU56WllXVWczTTFBeEdEQVdCZ05W\nQkFvTUQwRnNhV05xWVNCWFlYSmphR0hGZ2pFTE1Ba0dBMVVFQmhNQ1ZWTXdn\nZ0VpTUEwR0NTcUdTSWIzRFFFQkFRVUFBNElCRHdBd2dnRUtBb0lCQVFEa3Uy\nVHY4VmRCaERPY1E2ckQ3NFlON1UwNDZzRjQ5VTVHc2tPSUNidVY4cFE0YktP\nUTR4YmpNMXEreFhhMTJsRXUzdzBYc09KMHltemQ2OUdsdjhFVnNiT1JUcHkr\nTHVuLzVVRGpITXdpc1RNYjM1U3RWRi9XekV0TStWUFA2VGg0TGhmOHRYTGNi\nbVpxdi91M2d5eHBUc3NKb1VkenRyUko2bGczUStSbXM0RWFEekY0V2lnZDdo\nNVBmRFR5bEpJOFlEWlF3WEpxeTc4NG1pRmQ2eTVaM1dRZHhaaldMOHBGc0RT\nK1ZpZndLWUU0QU5tZHorZ2pEUEkrREJ0dHBrdDdZYWxVVmFLYkdBTW8zbjdV\nUkY4eEhkZm1yUk93MkJzd21WTnBDNkdMdFQzVVRpQVNKSjMxS0JVUmI1aWVI\nTTRKQTVLc2wxbk9yWkxSTXhtNjRxR0hBZ01CQUFHamdnSHhNSUlCN1RBTUJn\nTlZIUk1CQWY4RUFqQUFNQjhHQTFVZEl3UVlNQmFBRklnbkZ3bXB0aGhnaSt6\ncnV2WkhXY1ZTVktPM01EOEdDQ3NHQVFVRkJ3RUJCRE13TVRBdkJnZ3JCZ0VG\nQlFjd0FZWWphSFIwY0RvdkwyOWpjM0F1WVhCd2JHVXVZMjl0TDI5amMzQXdN\neTEzZDJSeU1URXdnZ0VkQmdOVkhTQUVnZ0VVTUlJQkVEQ0NBUXdHQ1NxR1NJ\nYjNZMlFGQVRDQi9qQ0J3d1lJS3dZQkJRVUhBZ0l3Z2JZTWdiTlNaV3hwWVc1\nalpTQnZiaUIwYUdseklHTmxjblJwWm1sallYUmxJR0o1SUdGdWVTQndZWEow\nZVNCaGMzTjFiV1Z6SUdGalkyVndkR0Z1WTJVZ2IyWWdkR2hsSUhSb1pXNGdZ\nWEJ3YkdsallXSnNaU0J6ZEdGdVpHRnlaQ0IwWlhKdGN5QmhibVFnWTI5dVpH\nbDBhVzl1Y3lCdlppQjFjMlVzSUdObGNuUnBabWxqWVhSbElIQnZiR2xqZVNC\naGJtUWdZMlZ5ZEdsbWFXTmhkR2x2YmlCd2NtRmpkR2xqWlNCemRHRjBaVzFs\nYm5SekxqQTJCZ2dyQmdFRkJRY0NBUllxYUhSMGNEb3ZMM2QzZHk1aGNIQnNa\nUzVqYjIwdlkyVnlkR2xtYVdOaGRHVmhkWFJvYjNKcGRIa3ZNQllHQTFVZEpR\nRUIvd1FNTUFvR0NDc0dBUVVGQndNRE1CMEdBMVVkRGdRV0JCVFFBUi9uZXVa\nV1dzOEUvYXc3QWhoR1ZOZWdzVEFPQmdOVkhROEJBZjhFQkFNQ0I0QXdFd1lL\nS29aSWh2ZGpaQVlCQkFFQi93UUNCUUF3RFFZSktvWklodmNOQVFFTEJRQURn\nZ0VCQUhka2FJMzN2aVBGc2xIWXBXTi8wdVorZVpxWWpPMkE3QStCeGxqMnFh\nTEY5STI0dngwMzUyd1g1KzBXWUh2eGlTTzY2VXdSV04rek85eFZ4dnlqeEdI\naGdQbGJzMVlldmszdDBWYUc5WXM5TUw5RHVVNllxWEU2YWo1ZnNuVG5yOEcx\nUnZHYkFqM2d4ZXRKRzlxTDU3ODdBUGNuSnp6NzlVdVBkQVhlVThZNW0xSzZU\nT0FzM2IzdFJ1SE02U0JwM1lDMVhqNGZLVUxLdXRHV2lVdXFDQnFsUFgydmt6\nQUkxZnp1U0NPZ2pORGdKSFZBQnBEWlpzaUxETHczWjJ2RGh5YWtOdXB5UENZ\nRE92T2xZblkrU000d1Z4OTVncUtDVW5ZWDgwSSs4U3F6ZWg4QU9Ib3d2d0ZU\nMHIxSWowTkJSMnhKN0NlK3BETzlveUtGd2VFYVQwOD08L2RhdGE+Cgk8L2Fy\ncmF5PgoKCgk8a2V5PkVudGl0bGVtZW50czwva2V5PgoJPGRpY3Q+CgkJPGtl\neT5rZXljaGFpbi1hY2Nlc3MtZ3JvdXBzPC9rZXk+CgkJPGFycmF5PgoJCQk8\nc3RyaW5nPlFMNzZYWUg3M1AuKjwvc3RyaW5nPgkJCgkJPC9hcnJheT4KCQk8\na2V5PmdldC10YXNrLWFsbG93PC9rZXk+CgkJPGZhbHNlLz4KCQk8a2V5PmFw\ncGxpY2F0aW9uLWlkZW50aWZpZXI8L2tleT4KCQk8c3RyaW5nPlFMNzZYWUg3\nM1AuY29tLm5pZWNoLnRvLnphZHppYWxhPC9zdHJpbmc+CgkJPGtleT5jb20u\nYXBwbGUuZGV2ZWxvcGVyLnRlYW0taWRlbnRpZmllcjwva2V5PgoJCTxzdHJp\nbmc+UUw3NlhZSDczUDwvc3RyaW5nPgoJCTxrZXk+YXBzLWVudmlyb25tZW50\nPC9rZXk+CgkJPHN0cmluZz5wcm9kdWN0aW9uPC9zdHJpbmc+CgkJPGtleT5i\nZXRhLXJlcG9ydHMtYWN0aXZlPC9rZXk+CgkJPHRydWUvPgoKCTwvZGljdD4K\nCTxrZXk+RXhwaXJhdGlvbkRhdGU8L2tleT4KCTxkYXRlPjIwMTktMTAtMDhU\nMTQ6MjA6MzlaPC9kYXRlPgoJPGtleT5OYW1lPC9rZXk+Cgk8c3RyaW5nPmNv\nbS5uaWVjaC50by56YWR6aWFsYSBBcHBTdG9yZTwvc3RyaW5nPgoJPGtleT5U\nZWFtSWRlbnRpZmllcjwva2V5PgoJPGFycmF5PgoJCTxzdHJpbmc+UUw3NlhZ\nSDczUDwvc3RyaW5nPgoJPC9hcnJheT4KCTxrZXk+VGVhbU5hbWU8L2tleT4K\nCTxzdHJpbmc+QWxpY2phIFdhcmNoYSYjMzIyOzwvc3RyaW5nPgoJPGtleT5U\naW1lVG9MaXZlPC9rZXk+Cgk8aW50ZWdlcj4zNjQ8L2ludGVnZXI+Cgk8a2V5\nPlVVSUQ8L2tleT4KCTxzdHJpbmc+NjYyMmQ5ODgtYjQyOS00Nzk1LTlkY2It\nM2I5ZjY5YmYzNGE2PC9zdHJpbmc+Cgk8a2V5PlZlcnNpb248L2tleT4KCTxp\nbnRlZ2VyPjE8L2ludGVnZXI+CjwvZGljdD4KPC9wbGlzdD6ggg2xMIID8zCC\nAtugAwIBAgIBFzANBgkqhkiG9w0BAQUFADBiMQswCQYDVQQGEwJVUzETMBEG\nA1UEChMKQXBwbGUgSW5jLjEmMCQGA1UECxMdQXBwbGUgQ2VydGlmaWNhdGlv\nbiBBdXRob3JpdHkxFjAUBgNVBAMTDUFwcGxlIFJvb3QgQ0EwHhcNMDcwNDEy\nMTc0MzI4Line truncated
|
||||
"provisioningProfileId": "WX8B857VDM",
|
||||
"pushPrivateSigningKey": "-----BEGIN RSA PRIVATE KEY-----\nMIIEpQIBAAKCAQEAokW6QiqVOm0gXmyY2aBVRNxeVeSg5u1j0wYSbMc64F90UUlJ\nwVcqDxQdAqo7z1RHTsUAazLriY5cqle+ivxjNTqKmvpmUd85ajMTSG6GaQPG4RHw\nAkRY+d221HUV9L81R0RY9VrOEhzqtoUmDGiv5AWz88xcVnU/3R59LSGqiV9nerP9\nDRkUHoAlW7Ga6YUKXtqdhSOspSZq/plar3oogGQD4WFKr2j1B1R2t8xuZoe2H956\nHxTzb4Y2jRZNuqME4R5dgsHIxvhQbAVYQCVPYrsGa+74WsXFBa6/kVSrRuLbYYIa\nfrqw1hTy0ujzQzcmOBOpBAiJb4ZfCM60rHaYcwIDAQABAoIBABbFCfm21/G21k/p\nlRkUcmKTOXl790fwD43Q0E0LN6u8OSoeyoaioHg1Lkztbi8NeGQJKKHiJ0LKAx6z\n6Ty/ZXxWlpRghnL+i6PnPtFvENUzfnied7sZtBJ7legB9e691dAGOyVSdI/+FsE9\ntbUQC35HfpS7HEnVcfypBo+Kpc9YpnfsKPXfk4lZTHnuE9G0pDE9Nhz3MSHdG3uz\nTOuQa+aKKcek38Y7dPJuioG/ffqlhoCFBLTwR/owil8cWx3THX7yvYzTPCAbosiY\nZdzM/JYgpi0kSoV30pnT6EfBl/qMtgo6jV6wrZ5FL8lR6iW/RsTbb4CMznLXyuMF\newsTuaECgYEA1y59P0LEfsyuqF/7nXohyPu4ddmQsxLrb4hQFxznFwEKHvNW8P9c\n+soCp0DBNZHENZG3r3GHMmcKnmdgqFjFQESqEwzTHjLSD5bipFOXXkf8ILownVpp\nkYnJVBWEFGsinZOinkMKg1t0Cp0lAmKsEGC1PLrk1vRuoW8RWenFcfUCgYEAwQ3k\n+95b5LA44lXyZ+T2ojY4HAdnYKeMO3eoAgRzdYVrCJ+vgK+fbnBx2pedUw0Ct/O3\n9imSvUoqoBAkfcqWTGdxmsITYuLFaD64g+XLQjGmZCQlQTrd0FWFYAYQEVeq2GQu\nx5WarZBgJ2N76ReDDbV0y34igAw7/lRbTn8GF8cCgYEArdPiXc5ECeKzXVVcX9Sf\nokqmsp005VRtWMYXXPR+QfkXAfW10C2aIQY7qn0HF41Poa67YkEl5DOVQm1kidpc\nThSqB4WerpCDXRCYugLFDua76HxAdXzqNyXFu+U9s3jwl7cB8TNbNM4WKdTUZt7q\n/LgC8LJR1svS6xpcO/Q7ClkCgYEAipi5DgGjI1lIXWWqs9/zGJ2oRqciFofy6zaK\nTXh4k6SHJRfPXam34gekFXTHo7oxe3LjXTyXdoHqfCNar1xj7tJNAvfWByYa5y4w\nNbEh7ZHLjRVOEbh7Sng7Wqa/nlO7+cJiuzKya2y9O0n2yW7xzUO6Z+VUS219HKff\nLUmQTD0CgYEAydaBypz1wWD5qpQCP8u3kfkmmfRMi9fL+kdfb2ldUUo3JVl1ikpW\ntjhELvzjHVuI39lDI4gHRjoUPMvLwUxhF0zB6CCl6D6g67TfWKecLy/RgttnwLf5\nlAFa1fzYVS+Z54nreohgbq4qsFzq3p6NbPQIejBskaqtfQIVb2vSGs8=\n-----END RSA PRIVATE KEY-----\n",
|
||||
"certId": "XLKCSBMJDV",
|
||||
"certP12": "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\n1eOAa/1MCq2acE8AkzAUz+XABAiLGr17TUyqog==\n",
|
||||
"certPassword": "AcovcuslT2sK2PNKxeK2Vw==",
|
||||
"certPrivateSigningKey": "-----BEGIN RSA PRIVATE KEY-----\nMIIEpAIBAAKCAQEA5Ltk7/FXQYQznEOqw++GDe1NOOrBePVORrJDiAm7lfKUOGyj\nkOMW4zNavsV2tdpRLt8NF7DidMps3evRpb/BFbGzkU6cvi7p/+VA4xzMIrEzG9+U\nrVRf1sxLTPlTz+k4eC4X/LVy3G5mar/7t4MsaU7LCaFHc7a0SepYN0PkZrOBGg8x\neFooHe4eT3w08pSSPGA2UMFyasu/OJohXesuWd1kHcWY1i/KRbA0vlYn8CmBOADZ\nnc/oIwzyPgwbbaZLe2GpVFWimxgDKN5+1ERfMR3X5q0TsNgbMJlTaQuhi7U91E4g\nEiSd9SgVEW+YnhzOCQOSrJdZzq2S0TMZuuKhhwIDAQABAoIBADmfM9h2liK9KTtO\nmLBvoPfVNkUGbuMH+UY6FmUo/obR66EjTTls3BE0mMHCgRti+lyyeFvlhcciT7bA\nd7onyn6qm7AZe8c1jmGW4XfVg1+iAFwfQRdBUDuOj9G+ZezUthMgmY5iL6N0dHyk\nDMe4VfHZlbDBXny6JEUSslKJ2fo8+HcgR8/+UQD9LA1fITVsbn6/T0lQd8ZyoX4J\nJ6VVhwkWIQUuJ+Ff2O/ttQGFrqWPxcQc0H07MAW7QM3Tc5laGRcOBGCLFw+XhIla\nVsfuO4JF2Uzxpq3YkmpFdW60ZL00groySmjKqZfF8wJqnOyBltmlZDPJrwzxfkYn\nKmy+hCECgYEA8liKuG2PA8Gme7IDUvUlk+GKDTwnN4PMrwLvcRm+T/wzHjpuWelr\nmTeXPOAd6xg16YD2pxOCg7Rd1CC+LifMfnwZ+oKnobRYrZzyMwn8RfLEu7luBbSs\nSsXmkVgxt7H4Ctn3bthcYUNJBFw9MFSYZBiESbvXlWJihbVh2RRG9U8CgYEA8Z5+\ncSk2vItyEuY85cPJ66rpS0bIa5tyLdC22ZjZeRKhVwxR3Ubj2XtnMh6esnNvzdzl\nWK8GYzVqwo1Agf3YxUHoFU2sBmE739gqsGsXc0MaUYEWL/NQP6RZwndosvdlz0vC\nUucmoVIFmbYCUZm0fdxWAs4caqp+axwqzgie8kkCgYA972z8d1t+fUY8BKGe2PIS\nmih6+22WXYO5Eon5ILgnnu+9gPdk2Sfbj36CW1CqHZThqUjgKDdJy5XTOc8A4sD7\nbmIBSNTombu0GxE3BA1KHyIyS9CXerYxoeQsLxr9tGkM9OmslXT+koCdsakdqfEM\nW8urQGt3TR6ax2FLZkhANwKBgQDWSvjfymrWjGKvKm6GZcCXeO0Sq4OkFKkOw1b4\n5Jy0yfY4ax/MPkxlHhKnnAqqQMG/dpQetJTYto6qYHb/E7IHOW3qPwo8Wk6tOF6c\nO08AFlqoQPH5szXQTCaj2B4O5GbylMGTn5leaI25B3lFACrZmera8Ay5+d7HhqLc\nOz8lqQKBgQDGVYe+CxAQ+LaM9VG8endl9br0Heop0/yDVm01I1oVy+lcF5ySuZmj\nvvKJtS7Dfc9NHKz+tht88uznaD6E3Nh3fT6J9w1lKsdgLToDcCs1B9pWiMaQWhJR\n2uz9a4pCn5E96rihIHzS1+SjnEGNgIVEieT6uSlnEf1MXJ0e/oGFXQ==\n-----END RSA PRIVATE KEY-----\n"
|
||||
},
|
||||
"manifest": {
|
||||
"android": {
|
||||
"package": "com.yourcompany.yourappname"
|
||||
},
|
||||
"bundledAssets": [
|
||||
"asset_744ce60078c17d86006dd0edabcd59a7.ttf",
|
||||
"asset_5221f35630f5b71e53c3196d3ff681a1.ttf",
|
||||
"asset_517bb9a13989451be6623299153f503f.ttf",
|
||||
"asset_b06871f281fee6b241d60582ae9369b9.ttf",
|
||||
"asset_e20945d7c929279ef7a6f1db184a4470.ttf",
|
||||
"asset_74c652671225d6ded874a648502e5f0a.ttf",
|
||||
"asset_1eccbc4c41d49fd81840aef3eaabe862.ttf",
|
||||
"asset_a37b0c01c0baf1888ca812cc0508f6e2.ttf",
|
||||
"asset_9e9dbe78c20054a6d71d9c10c939ba68.ttf",
|
||||
"asset_d2285965fe34b05465047401b8595dd0.ttf",
|
||||
"asset_5cdf883b18a5651a29a4d1ef276d2457.ttf",
|
||||
"asset_7d40544b395c5949f4646f5e150fe020.png",
|
||||
"asset_cdd04e13d4ec83ff0cd13ec8dabdc341.png",
|
||||
"asset_a132ecc4ba5c1517ff83c0fb321bc7fc.png",
|
||||
"asset_0ea69b5077e7c4696db85dbcba75b0e1.png",
|
||||
"asset_f5b790e2ac193b3d41015edb3551f9b8.png",
|
||||
"asset_755fce460f64aed4d3f3ac1a6249ceea.png",
|
||||
"asset_45a8ce76165dcaec1c5e39be15478e53.png",
|
||||
"asset_2d8deea13c41b52b04442d91fb31c2c5.png",
|
||||
"asset_5c36108361d0a750fd25533356f80245.png",
|
||||
"asset_af47141a24eb8e5751dfd034159479bd.png",
|
||||
"asset_ad107d1ca027259ca50ce0f432293e6c.png",
|
||||
"asset_a30cd76fa74ee5a0bab5aba37c101416.png",
|
||||
"asset_49a79d66bdea2debf1832bf4d7aca127.ttf",
|
||||
"asset_778ffc9fe8773a878e9c30a6304784de.png",
|
||||
"asset_376d6a4c7f622917c39feb23671ef71d.png",
|
||||
"asset_c79c3606a1cf168006ad3979763c7e0c.png",
|
||||
"asset_02bc1fa7c0313217bde2d65ccbff40c9.png",
|
||||
"asset_35ba0eaec5a4f5ed12ca16fabeae451d.png"
|
||||
],
|
||||
"description": "A very interesting project.",
|
||||
"icon": "./assets/images/icon.png",
|
||||
"iconUrl": "https://d1wp6m56sqw74a.cloudfront.net/~assets/fa6577fecc0a7838f15a254577639984",
|
||||
"ios": {
|
||||
"bundleIdentifier": "com.niech.to.zadziala",
|
||||
"supportsTablet": true
|
||||
},
|
||||
"locales": {},
|
||||
"name": "testApp",
|
||||
"orientation": "portrait",
|
||||
"platforms": [
|
||||
"ios",
|
||||
"android"
|
||||
],
|
||||
"privacy": "public",
|
||||
"sdkVersion": "29.0.0",
|
||||
"slug": "testApp",
|
||||
"splash": {
|
||||
"backgroundColor": "#ffffff",
|
||||
"image": "./assets/images/splash.png",
|
||||
"imageUrl": "https://d1wp6m56sqw74a.cloudfront.net/~assets/43ec0dcbe5a156bf9e650bb8c15e7af6",
|
||||
"resizeMode": "contain"
|
||||
},
|
||||
"updates": {
|
||||
"fallbackToCacheTimeout": 0
|
||||
},
|
||||
"version": "1.0.0",
|
||||
"id": "@admin/testApp",
|
||||
"revisionId": "1.0.0-r.zV1W89wn23",
|
||||
"publishedTime": "2018-10-16T08:44:05.362Z",
|
||||
"commitTime": "2018-10-16T08:44:05.403Z",
|
||||
"bundleUrl": "https://d1wp6m56sqw74a.cloudfront.net/%40admin%2FtestApp%2F1.0.0%2Fb8678e2be7cd5bb9e240db0dd3c691c0-29.0.0-ios.js",
|
||||
"releaseChannel": "default",
|
||||
"hostUri": "exp.host/@admin/testApp"
|
||||
},
|
||||
"sdkVersion": "29.0.0",
|
||||
"messageCreatedTimestamp": 1539679447643
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
import * as context from 'turtle/turtleContext';
|
||||
|
||||
test('Initially should not exit', () => {
|
||||
expect(context.checkShouldExit()).toBe(false);
|
||||
});
|
||||
@@ -1,14 +0,0 @@
|
||||
import * as validator from 'turtle/validator';
|
||||
|
||||
const testValidator = (fixturePath: string) => {
|
||||
const data = require(fixturePath);
|
||||
expect(() => validator.sanitizeJob(data)).not.toThrow();
|
||||
};
|
||||
|
||||
test('Passes valid manifest for Android', () => {
|
||||
testValidator('turtle/__tests__/fixtures/androidJob.json');
|
||||
});
|
||||
|
||||
test('Passes valid manifest for ios', () => {
|
||||
testValidator('turtle/__tests__/fixtures/iosJob.json');
|
||||
});
|
||||
@@ -1,2 +0,0 @@
|
||||
export const EXTRA_DIMENSIONS = Symbol('Extra Dimensions');
|
||||
export const GROUP_HASH = Symbol('Group Hash');
|
||||
@@ -1,15 +0,0 @@
|
||||
import config from 'turtle/config';
|
||||
|
||||
export const TYPE_DIMENSIONS = (type: string) => [
|
||||
[
|
||||
{ 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: 'host', Value: config.hostname },
|
||||
],
|
||||
];
|
||||
@@ -1,186 +0,0 @@
|
||||
import aws from 'aws-sdk';
|
||||
import _ from 'lodash';
|
||||
|
||||
import { EXTRA_DIMENSIONS, GROUP_HASH } from 'turtle/aws/cloudwatch/constants';
|
||||
import calculateStatistics from 'turtle/aws/cloudwatch/statistics';
|
||||
import {
|
||||
IMetric,
|
||||
IMetricConfiguration,
|
||||
IMetricData,
|
||||
IMetricsChunk,
|
||||
MetricRegistrationObject,
|
||||
} from 'turtle/aws/cloudwatch/types';
|
||||
import config from 'turtle/config';
|
||||
import logger from 'turtle/logger';
|
||||
|
||||
interface IState {
|
||||
metrics: IMetric[];
|
||||
registeredMetrics: {
|
||||
[key: string]: IMetricConfiguration;
|
||||
};
|
||||
intervalId: NodeJS.Timer | null;
|
||||
}
|
||||
|
||||
const SUCCESS_SUFFIX = '.success';
|
||||
const FAIL_SUFFIX = '.fail';
|
||||
const CHUNK_SIZE = 20;
|
||||
|
||||
const state: IState = {
|
||||
metrics: [],
|
||||
registeredMetrics: {},
|
||||
intervalId: null,
|
||||
};
|
||||
|
||||
const cloudWatch = new aws.CloudWatch({
|
||||
apiVersion: '2010-08-01',
|
||||
region: config.cloudwatch.region,
|
||||
accessKeyId: config.aws.accessKeyId,
|
||||
secretAccessKey: config.aws.secretAccessKey,
|
||||
});
|
||||
|
||||
export function init() {
|
||||
if (config.cloudwatch.disabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
state.intervalId = setInterval(_dumpMetrics, config.cloudwatch.intervalMs);
|
||||
}
|
||||
|
||||
export function addMetricData({
|
||||
name,
|
||||
value,
|
||||
success = true,
|
||||
explicitName = false,
|
||||
extraDimensions = [],
|
||||
}: IMetricData) {
|
||||
if (config.cloudwatch.disabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
const metricName = explicitName ? name : _createMetricName(name, success);
|
||||
const registeredMetric = state.registeredMetrics[metricName];
|
||||
if (!registeredMetric) {
|
||||
return;
|
||||
}
|
||||
const { unit } = registeredMetric;
|
||||
const metric: IMetric = {
|
||||
MetricName: metricName,
|
||||
Unit: unit,
|
||||
Value: value,
|
||||
Timestamp: new Date(),
|
||||
[EXTRA_DIMENSIONS]: extraDimensions,
|
||||
[GROUP_HASH]: _calculateGroupHash(metricName, extraDimensions),
|
||||
};
|
||||
state.metrics.push(metric);
|
||||
}
|
||||
|
||||
export function registerMetric({
|
||||
metricName,
|
||||
unit,
|
||||
reducer,
|
||||
dimensions = [],
|
||||
statistics = false,
|
||||
addEmpty = true,
|
||||
explicitName = false,
|
||||
}: MetricRegistrationObject) {
|
||||
if (config.cloudwatch.disabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
const successMetricName = _createMetricName(metricName, true, explicitName);
|
||||
state.registeredMetrics[successMetricName] = { unit, statistics, reducer, addEmpty, dimensions };
|
||||
if (!explicitName) {
|
||||
const failMetricName = _createMetricName(metricName, false);
|
||||
state.registeredMetrics[failMetricName] = { unit, statistics, reducer, addEmpty, dimensions };
|
||||
}
|
||||
}
|
||||
|
||||
function _createMetricName(name: string, success: boolean, explicitName?: boolean) {
|
||||
if (explicitName) {
|
||||
return name;
|
||||
} else {
|
||||
const suffix = success ? SUCCESS_SUFFIX : FAIL_SUFFIX;
|
||||
return `${name}${suffix}`;
|
||||
}
|
||||
}
|
||||
|
||||
function _calculateGroupHash(metricName: string, extraDimensions: any[]) {
|
||||
const dimString = _.flatten(extraDimensions)
|
||||
.map(({ Name, Value }) => `,${Name}:${Value}`)
|
||||
.join('');
|
||||
return `${metricName}${dimString}`;
|
||||
}
|
||||
|
||||
function _dumpMetrics() {
|
||||
if (!state.metrics.length && Object.keys(state.registeredMetrics).length < 0) {
|
||||
// empty metrics list, skipping
|
||||
return;
|
||||
}
|
||||
|
||||
logger.trace(
|
||||
'[cloudwatch] lenghts for state.metrics=%d, state.registeredMetrics=%d',
|
||||
state.metrics.length,
|
||||
Object.keys(state.registeredMetrics).length,
|
||||
);
|
||||
|
||||
_addEmptyMetrics(state.metrics);
|
||||
const formattedMetrics = _formatMetrics(state.metrics);
|
||||
_pushMetrics(formattedMetrics);
|
||||
|
||||
state.metrics = [];
|
||||
}
|
||||
|
||||
function _addEmptyMetrics(metrics: IMetric[]) {
|
||||
const seenMetrics = metrics.reduce((acc, i) => {
|
||||
if (!acc.has(i.MetricName)) {
|
||||
acc.add(i.MetricName);
|
||||
}
|
||||
return acc;
|
||||
}, new Set());
|
||||
|
||||
const nonEmptyMetrics = _.omitBy(state.registeredMetrics, ({ addEmpty }) => !addEmpty);
|
||||
const toAdd = _.difference(Object.keys(nonEmptyMetrics), Array.from(seenMetrics));
|
||||
toAdd.forEach((name) => addMetricData({ name, value: 0, explicitName: true }));
|
||||
}
|
||||
|
||||
function _formatMetrics(metrics: IMetric[]) {
|
||||
const reducedMetrics = _reduceMetrics(metrics);
|
||||
const metricsWithDimensions = reducedMetrics.map((metric) => {
|
||||
const { dimensions } = state.registeredMetrics[metric.MetricName];
|
||||
const joinedDimensions = [...dimensions, ...metric[EXTRA_DIMENSIONS]];
|
||||
return joinedDimensions.map((i) => Object.assign({}, metric, { Dimensions: i }));
|
||||
});
|
||||
const metricData = _.flatten(metricsWithDimensions);
|
||||
const chunks = _.chunk(metricData, CHUNK_SIZE);
|
||||
return chunks.map((chunk) => ({
|
||||
Namespace: config.cloudwatch.namespace,
|
||||
MetricData: chunk,
|
||||
}));
|
||||
}
|
||||
|
||||
function _reduceMetrics(metrics: IMetric[]) {
|
||||
const grouped = _.groupBy(metrics, GROUP_HASH);
|
||||
return _.reduce(
|
||||
grouped,
|
||||
(acc: IMetric[], val: any) => {
|
||||
const { MetricName: key } = val[0] as IMetric;
|
||||
const { reducer, statistics } = state.registeredMetrics[key];
|
||||
const reduced = reducer ? reducer(val) : val;
|
||||
const withStats = statistics ? calculateStatistics(reduced) : reduced;
|
||||
acc.push(...withStats);
|
||||
return acc;
|
||||
},
|
||||
[],
|
||||
);
|
||||
}
|
||||
|
||||
function _pushMetrics(metrics: IMetricsChunk[]) {
|
||||
logger.trace('[cloudwatch] pushing metrics', JSON.stringify(metrics, null, '\t'));
|
||||
metrics.forEach((metricsChunk) => {
|
||||
cloudWatch.putMetricData(metricsChunk, (err) => {
|
||||
if (err) {
|
||||
logger.warn('[cloudwatch]', err);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
import { IMetric } from 'turtle/aws/cloudwatch/types';
|
||||
|
||||
export function sum(metrics: IMetric[]) {
|
||||
if (metrics.length === 0) {
|
||||
return [];
|
||||
}
|
||||
const reduced = metrics.reduce((acc: IMetric | null, i: IMetric) => {
|
||||
let result = acc;
|
||||
if (!result) {
|
||||
result = {...i};
|
||||
} else {
|
||||
result.Value += i.Value;
|
||||
}
|
||||
return result;
|
||||
}, null);
|
||||
return [reduced];
|
||||
}
|
||||
|
||||
export function avg(metrics: IMetric[]) {
|
||||
if (!metrics.length) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const metric = sum(metrics)[0] as IMetric;
|
||||
metric.Value = metric.Value / metrics.length;
|
||||
return [metric];
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
import { IMetric, MetricWithStats } from 'turtle/aws/cloudwatch/types';
|
||||
|
||||
export default function calculateStatistics(metrics: IMetric[]) {
|
||||
if (!metrics.length) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const reduced = metrics.reduce((acc: MetricWithStats | null, i: IMetric) => {
|
||||
let result = acc;
|
||||
if (!result) {
|
||||
result = Object.assign({}, i, {
|
||||
StatisticValues: {
|
||||
Maximum: -Infinity,
|
||||
Minimum: Infinity,
|
||||
SampleCount: 0,
|
||||
Sum: 0.0,
|
||||
},
|
||||
Value: undefined,
|
||||
});
|
||||
}
|
||||
|
||||
result.StatisticValues.Maximum = Math.max(result.StatisticValues.Maximum, i.Value);
|
||||
result.StatisticValues.Minimum = Math.min(result.StatisticValues.Minimum, i.Value);
|
||||
result.StatisticValues.SampleCount += 1;
|
||||
result.StatisticValues.Sum += i.Value;
|
||||
return result;
|
||||
}, null);
|
||||
return [reduced];
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
import { EXTRA_DIMENSIONS, GROUP_HASH } from 'turtle/aws/cloudwatch/constants';
|
||||
|
||||
export interface IMetric {
|
||||
MetricName: string;
|
||||
Unit: string;
|
||||
Value: number;
|
||||
Timestamp: Date;
|
||||
StatisticValues?: {
|
||||
Maximum: number;
|
||||
Minimum: number;
|
||||
SampleCount: number;
|
||||
Sum: number;
|
||||
};
|
||||
[EXTRA_DIMENSIONS]: any;
|
||||
[GROUP_HASH]: string;
|
||||
}
|
||||
|
||||
export type MetricWithStats = {
|
||||
StatisticValues: {
|
||||
Maximum: number;
|
||||
Minimum: number;
|
||||
SampleCount: number;
|
||||
Sum: number;
|
||||
},
|
||||
} & IMetric;
|
||||
|
||||
export interface IMetricData {
|
||||
name: string;
|
||||
value: number;
|
||||
success?: boolean;
|
||||
explicitName?: boolean;
|
||||
extraDimensions?: any;
|
||||
}
|
||||
|
||||
export interface IMetricConfiguration {
|
||||
unit: string;
|
||||
statistics?: boolean;
|
||||
reducer?: any;
|
||||
addEmpty?: boolean;
|
||||
dimensions?: any;
|
||||
}
|
||||
|
||||
export type MetricRegistrationObject = { metricName: string, explicitName?: boolean } & IMetricConfiguration;
|
||||
|
||||
export interface IMetricsChunk {
|
||||
Namespace: string;
|
||||
MetricData: IMetric[];
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
import AWS from 'aws-sdk';
|
||||
import fs from 'fs-extra';
|
||||
import _ from 'lodash';
|
||||
|
||||
import config from 'turtle/config';
|
||||
|
||||
interface IFileUploadParams {
|
||||
bucketName?: string;
|
||||
key: string;
|
||||
srcPath: string;
|
||||
}
|
||||
|
||||
const s3 = new AWS.S3({
|
||||
..._.pick(config.aws, ['accessKeyId', 'secretAccessKey']),
|
||||
..._.pick(config.s3, ['region']),
|
||||
});
|
||||
|
||||
export default s3;
|
||||
|
||||
export async function uploadFile({ bucketName = config.s3.bucket, key, srcPath }: IFileUploadParams) {
|
||||
return await s3
|
||||
.upload({
|
||||
Bucket: bucketName,
|
||||
Key: key,
|
||||
Body: fs.createReadStream(srcPath),
|
||||
})
|
||||
.promise();
|
||||
}
|
||||
@@ -1,91 +0,0 @@
|
||||
import retry from 'async-retry';
|
||||
import AWS from 'aws-sdk';
|
||||
import _ from 'lodash';
|
||||
|
||||
import { OUTPUT_QUEUE_URL, QUEUE_URL } from 'turtle/aws/utils';
|
||||
import config from 'turtle/config';
|
||||
import { JOB_UPDATE_TYPE } from 'turtle/constants/build';
|
||||
import logger from 'turtle/logger';
|
||||
import { getCurrentJobId } from 'turtle/turtleContext';
|
||||
|
||||
const sqs = new AWS.SQS({
|
||||
apiVersion: '2012-11-05',
|
||||
..._.pick(config.aws, ['accessKeyId', 'secretAccessKey']),
|
||||
..._.pick(config.sqs, ['region']),
|
||||
});
|
||||
|
||||
const VISIBILITY_TIMEOUT_SEC = 30;
|
||||
|
||||
export async function receiveMessage(priority: string) {
|
||||
const params = {
|
||||
MaxNumberOfMessages: 1,
|
||||
QueueUrl: QUEUE_URL(priority),
|
||||
VisibilityTimeout: VISIBILITY_TIMEOUT_SEC,
|
||||
};
|
||||
return await retry(() => {
|
||||
return sqs
|
||||
.receiveMessage(params)
|
||||
.promise()
|
||||
.then((data) => {
|
||||
if (data.Messages) {
|
||||
logger.info(`Received a message`);
|
||||
return data.Messages[0];
|
||||
} else {
|
||||
logger.debug(`No messages received [${priority}]`);
|
||||
return null;
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
logger.error('Receive error', err);
|
||||
throw err;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export async function deleteMessage(priority: string, receiptHandle: string) {
|
||||
const params = {
|
||||
QueueUrl: QUEUE_URL(priority),
|
||||
ReceiptHandle: receiptHandle,
|
||||
};
|
||||
return await retry(() => {
|
||||
return sqs.deleteMessage(params).promise();
|
||||
});
|
||||
}
|
||||
|
||||
export async function changeMessageVisibility(priority: string, receiptHandle: string) {
|
||||
const params = {
|
||||
QueueUrl: QUEUE_URL(priority),
|
||||
ReceiptHandle: receiptHandle,
|
||||
VisibilityTimeout: VISIBILITY_TIMEOUT_SEC,
|
||||
};
|
||||
return sqs.changeMessageVisibility(params).promise();
|
||||
}
|
||||
|
||||
// Every VISIBILITY_TIMEOUT_SEC / 3 seconds we are telling AWS SQS
|
||||
// that we're still proccessing the message, so it does not
|
||||
// send the build job to another turtle agent
|
||||
export function changeMessageVisibilityRecurring(priority: string, receiptHandle: string, jobId: string) {
|
||||
return setInterval(() => {
|
||||
if (getCurrentJobId() === jobId) {
|
||||
changeMessageVisibility(priority, receiptHandle).catch((err) => {
|
||||
logger.warn('Error at change msg visibility', err);
|
||||
});
|
||||
}
|
||||
}, (VISIBILITY_TIMEOUT_SEC * 1000) / 3);
|
||||
}
|
||||
|
||||
export async function sendMessage(jobId: string, status: JOB_UPDATE_TYPE, data = {}) {
|
||||
const params = {
|
||||
MessageBody: JSON.stringify({ jobId, status, ...data }),
|
||||
QueueUrl: OUTPUT_QUEUE_URL(),
|
||||
DelaySeconds: 0,
|
||||
MessageGroupId: jobId,
|
||||
};
|
||||
try {
|
||||
return await retry(async () => {
|
||||
return await sqs.sendMessage(params).promise();
|
||||
});
|
||||
} catch (err) {
|
||||
logger.error(`Error sending SQS message: ${JSON.stringify(params)}}`, err);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
import config from 'turtle/config';
|
||||
|
||||
export const QUEUE_URL = (priority: string) => (config.sqs.queues as any)[priority][config.platform];
|
||||
export const OUTPUT_QUEUE_URL = () => config.sqs.queues.out;
|
||||
@@ -1,2 +0,0 @@
|
||||
export class ErrorWithCommandHelp extends Error {
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
export class ErrorWithProgramHelp extends Error {
|
||||
}
|
||||
@@ -1,74 +0,0 @@
|
||||
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/android';
|
||||
import { PLATFORMS } from 'turtle/constants';
|
||||
|
||||
export default (program: any, setCommonCommandOptions: any) => {
|
||||
const command = program.command('build:android [project-dir]');
|
||||
setCommonCommandOptions(command);
|
||||
command
|
||||
.alias('ba')
|
||||
.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)',
|
||||
)
|
||||
.option('--keystore-alias <alias>', 'keystore Alias')
|
||||
.description(
|
||||
'Build a standalone APK for your project, signed and ready for submission to the Google Play Store.',
|
||||
)
|
||||
.asyncAction(
|
||||
createBuilderAction({
|
||||
program,
|
||||
command,
|
||||
prepareCredentials,
|
||||
buildJobObject,
|
||||
builder,
|
||||
platform: PLATFORMS.ANDROID,
|
||||
os: ['darwin', 'linux'],
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
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;
|
||||
const keyPassword = process.env.EXPO_ANDROID_KEY_PASSWORD;
|
||||
|
||||
const someCredentialsExist = keystorePath || keystoreAlias || keystorePassword || keyPassword;
|
||||
const credentialsExist = keystorePath && keystoreAlias && keystorePassword && keyPassword;
|
||||
if (!credentialsExist) {
|
||||
if (someCredentialsExist) {
|
||||
throw new ErrorWithCommandHelp(
|
||||
'Please provide all required credentials - Keystore (with password), Keystore alias and Key password',
|
||||
);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
keystore: (await fs.readFile(keystorePath)).toString('base64'),
|
||||
keystoreAlias,
|
||||
keystorePassword,
|
||||
keyPassword,
|
||||
};
|
||||
}
|
||||
};
|
||||
@@ -1,29 +0,0 @@
|
||||
import { default as buildAndroid } from 'turtle/bin/commands/build/android';
|
||||
import { default as buildIOS } from 'turtle/bin/commands/build/ios';
|
||||
|
||||
function createCommand(builder: any) {
|
||||
return (program: any) => {
|
||||
return builder(program, setCommonCommandOptions);
|
||||
};
|
||||
}
|
||||
|
||||
function setCommonCommandOptions(cmd: any) {
|
||||
cmd
|
||||
.option('-u --username <username>', 'username (you can also set EXPO_USERNAME env variable)')
|
||||
.option('-p --password <password>', 'password (you can also set EXPO_PASSWORD env variable)')
|
||||
.option('-d --build-dir <build-dir>', 'directory for build artifact (default: `~/expo-apps`)')
|
||||
.option('-o --output <output-file-path>', 'output file path')
|
||||
.option(
|
||||
'--public-url <url>',
|
||||
'the URL of an externally hosted manifest (for self-hosted apps).',
|
||||
)
|
||||
.option(
|
||||
'--release-channel <channel-name>',
|
||||
'pull from specified release channel (default: default)',
|
||||
);
|
||||
}
|
||||
|
||||
export default {
|
||||
ios: createCommand(buildIOS),
|
||||
android: createCommand(buildAndroid),
|
||||
};
|
||||
@@ -1,82 +0,0 @@
|
||||
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';
|
||||
|
||||
export default (program: any, setCommonCommandOptions: any) => {
|
||||
const command = program.command('build:ios [project-dir]');
|
||||
setCommonCommandOptions(command);
|
||||
command
|
||||
.alias('bi')
|
||||
.option(
|
||||
'-t --type <build>',
|
||||
'type of build: archive|simulator',
|
||||
/^(archive|simulator)$/i,
|
||||
'archive',
|
||||
)
|
||||
.option('--team-id <apple-teamId>', 'Apple Team ID')
|
||||
.option(
|
||||
'--dist-p12-path <dist.p12>',
|
||||
'path to your Distribution Certificate P12 (please provide password as EXPO_IOS_DIST_P12_PASSWORD env variable)',
|
||||
)
|
||||
.option('--provisioning-profile-path <.mobileprovision>', 'path to your Provisioning Profile')
|
||||
.description(
|
||||
'Build a standalone IPA for your project, signed and ready for submission to the Apple App Store.',
|
||||
)
|
||||
.asyncAction(
|
||||
createBuilderAction({
|
||||
program,
|
||||
command,
|
||||
prepareCredentials,
|
||||
buildJobObject,
|
||||
builder,
|
||||
platform: PLATFORMS.IOS,
|
||||
os: 'darwin',
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
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) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { teamId, distP12Path, provisioningProfilePath } = cmd;
|
||||
const certPassword = process.env.EXPO_IOS_DIST_P12_PASSWORD;
|
||||
|
||||
const credentialsExist =
|
||||
teamId && distP12Path && certPassword && provisioningProfilePath;
|
||||
|
||||
if (!credentialsExist) {
|
||||
throw new ErrorWithCommandHelp(
|
||||
'Please provide all required credentials'
|
||||
+ '- Apple Team ID, Distribution Certificate P12 (with password) and Provisioning Profile',
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
teamId,
|
||||
certP12: (await fs.readFile(distP12Path)).toString('base64'),
|
||||
certPassword,
|
||||
provisioningProfile: (await fs.readFile(provisioningProfilePath)).toString('base64'),
|
||||
};
|
||||
};
|
||||
@@ -1,8 +0,0 @@
|
||||
import builders from 'turtle/bin/commands/build';
|
||||
import setup from 'turtle/bin/commands/setup';
|
||||
|
||||
export const setupIOS = setup.ios;
|
||||
export const setupAndroid = setup.android;
|
||||
|
||||
export const buildIOS = builders.ios;
|
||||
export const buildAndroid = builders.android;
|
||||
@@ -1,50 +0,0 @@
|
||||
import _ from 'lodash';
|
||||
|
||||
import { ErrorWithCommandHelp } from 'turtle/bin/commands/ErrorWithCommandHelp';
|
||||
import { ErrorWithProgramHelp } from 'turtle/bin/commands/ErrorWithProgramHelp';
|
||||
import setup from 'turtle/bin/setup/setup';
|
||||
import { PLATFORMS } from 'turtle/constants';
|
||||
import logger from 'turtle/logger';
|
||||
|
||||
const l = logger.withFields({ buildPhase: 'setting up environment' });
|
||||
|
||||
function createSetupCommand(platform: string, os?: string) {
|
||||
return (program: any) => {
|
||||
const command = program.command(`setup:${platform}`);
|
||||
const platformFirstLetter = platform[0];
|
||||
command
|
||||
.alias(`s${platformFirstLetter}`)
|
||||
.description(`Setup environment for building ${_.capitalize(platform)} standalone apps.`)
|
||||
.option(
|
||||
'--sdk-version <sdk-version>',
|
||||
'version of Expo SDK shell app to download (optional)',
|
||||
)
|
||||
.asyncAction(async (cmd: any) => await setupAction(program, cmd, platform, os));
|
||||
};
|
||||
}
|
||||
|
||||
async function setupAction(program: any, cmd: any, platform: string, os?: string) {
|
||||
try {
|
||||
if (os && process.platform !== os) {
|
||||
throw new Error('We don\'t support running standalone app builds for this platform on your operating system');
|
||||
}
|
||||
|
||||
const { sdkVersion } = cmd;
|
||||
await setup(platform, sdkVersion);
|
||||
l.info('it\'s all set!');
|
||||
} catch (err) {
|
||||
logger.error(`Failed to setup environment for ${platform} builds`);
|
||||
logger.error(err.stack);
|
||||
if (err instanceof ErrorWithCommandHelp) {
|
||||
cmd.help();
|
||||
} else if (err instanceof ErrorWithProgramHelp) {
|
||||
program.help();
|
||||
}
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
ios: createSetupCommand(PLATFORMS.IOS, 'darwin'),
|
||||
android: createSetupCommand(PLATFORMS.ANDROID),
|
||||
};
|
||||
@@ -1,51 +0,0 @@
|
||||
import os from 'os';
|
||||
import path from 'path';
|
||||
|
||||
import _ from 'lodash';
|
||||
|
||||
import { env } from 'turtle/utils/env';
|
||||
|
||||
const resolveEnv = () => {
|
||||
if (env('EXPO_LOCAL', '0') === '1') {
|
||||
return 'local';
|
||||
} else if (env('EXPO_STAGING', '0') === '1') {
|
||||
return 'staging';
|
||||
} else {
|
||||
return 'production';
|
||||
}
|
||||
};
|
||||
|
||||
const currentEnv = resolveEnv();
|
||||
const isLocal = (envName: string) => envName === 'local';
|
||||
const isStaging = (envName: string) => envName === 'staging';
|
||||
|
||||
const apiConfig = {
|
||||
protocol: 'https',
|
||||
hostname: 'exp.host',
|
||||
port: 443,
|
||||
};
|
||||
|
||||
if (isStaging(currentEnv)) {
|
||||
apiConfig.hostname = 'staging.exp.host';
|
||||
} else if (isLocal(currentEnv)) {
|
||||
apiConfig.protocol = 'http';
|
||||
apiConfig.hostname = 'localhost';
|
||||
apiConfig.port = 3000;
|
||||
}
|
||||
|
||||
const OFFLINE_ENV_VARS = {
|
||||
TURTLE_MODE: 'offline',
|
||||
API_PROTOCOL: apiConfig.protocol,
|
||||
API_HOSTNAME: apiConfig.hostname,
|
||||
API_PORT: apiConfig.port,
|
||||
TURTLE_FAKE_UPLOAD: '1',
|
||||
TURTLE_FAKE_UPLOAD_DIR: path.join(os.homedir(), 'expo-apps'),
|
||||
TURTLE_WORKING_DIR_PATH: path.join(os.homedir(), '.turtle/workingdir'),
|
||||
EXPO_SKIP_SOURCING: '1',
|
||||
};
|
||||
|
||||
export function initOfflineEnv() {
|
||||
_.map(OFFLINE_ENV_VARS, (val, key) => {
|
||||
process.env[key] = String(val);
|
||||
});
|
||||
}
|
||||
@@ -1,70 +0,0 @@
|
||||
import path from 'path';
|
||||
|
||||
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';
|
||||
|
||||
const { name, version } = JSON.parse(fs.readFileSync(path.join(__dirname, '../../package.json'), 'utf-8'));
|
||||
|
||||
const ModuleVersionChecker = ModuleVersion.createModuleVersionChecker(name, version);
|
||||
|
||||
LoggerDetach.configure(logger);
|
||||
|
||||
Command.prototype.asyncAction = function asyncAction(asyncFn: (...asyncFnArgs: any[]) => void): Command {
|
||||
return this.action(async (...args) => {
|
||||
try {
|
||||
await checkForUpdateAsync();
|
||||
} catch (e) {
|
||||
logger.warn('Failed to check for turtle-cli update.');
|
||||
}
|
||||
return await asyncFn(...args);
|
||||
});
|
||||
};
|
||||
|
||||
export function run(programName: string) {
|
||||
runAsync(programName).catch((e) => {
|
||||
logger.error('Uncaught Error', e);
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
|
||||
async function runAsync(programName: string) {
|
||||
program.version(version);
|
||||
Object.values(commands).forEach((command) => registerCommand(program, command));
|
||||
program.parse(process.argv);
|
||||
|
||||
const subCommand = process.argv[2];
|
||||
if (subCommand) {
|
||||
const commandNames = program.commands.reduce((acc: string[], command: any) => {
|
||||
acc.push(command._name);
|
||||
const alias = command._alias;
|
||||
if (alias) {
|
||||
acc.push(alias);
|
||||
}
|
||||
return acc;
|
||||
}, []);
|
||||
if (!commandNames.includes(subCommand)) {
|
||||
logger.error(
|
||||
`"${subCommand}" is not an ${programName} command. See "${programName} --help" for the full list of commands.`,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
program.help();
|
||||
}
|
||||
}
|
||||
|
||||
const registerCommand = (prog: any, command: any) => command(prog);
|
||||
|
||||
async function checkForUpdateAsync() {
|
||||
const { updateIsAvailable, current, latest } = await ModuleVersionChecker.checkAsync();
|
||||
if (updateIsAvailable) {
|
||||
logger.warn(
|
||||
`There is a new version of ${name} available (${latest}).
|
||||
You are currently using ${name} ${current}
|
||||
Run \`npm install -g ${name}\` to get the latest version`,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,114 +0,0 @@
|
||||
import path from 'path';
|
||||
import util from 'util';
|
||||
|
||||
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';
|
||||
import { checkSystem, ensureShellAppIsPresent } from 'turtle/bin/setup/utils/common';
|
||||
import { IToolDefinition } from 'turtle/bin/setup/utils/toolsDetector';
|
||||
import { formatShellAppDirectory } from 'turtle/builders/utils/android/workingdir';
|
||||
import config from 'turtle/config';
|
||||
import { PLATFORMS } from 'turtle/constants';
|
||||
import logger from 'turtle/logger';
|
||||
|
||||
const which = util.promisify(_which);
|
||||
const REQUIRED_TOOLS: IToolDefinition[] = [
|
||||
{
|
||||
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/',
|
||||
testFn: async () => {
|
||||
const { status, stdout } = await ExponentTools.spawnAsyncThrowError(
|
||||
'javac',
|
||||
['-version'],
|
||||
{ stdio: 'pipe' },
|
||||
);
|
||||
if (stdout.match(/no java runtime present/i)) {
|
||||
return false;
|
||||
}
|
||||
return status === 0;
|
||||
},
|
||||
},
|
||||
];
|
||||
const LOGGER_FIELDS = { buildPhase: 'setting up environment' };
|
||||
const l = logger.withFields(LOGGER_FIELDS);
|
||||
|
||||
export default async function setup(sdkVersion?: string) {
|
||||
await checkSystem(REQUIRED_TOOLS);
|
||||
await prepareAndroidEnv();
|
||||
if (sdkVersion) {
|
||||
await ensureShellAppIsPresent(
|
||||
sdkVersion,
|
||||
{ formatShellAppDirectory, formatShellAppTarballUriPath },
|
||||
_shellAppPostDownloadAction,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async function prepareAndroidEnv() {
|
||||
await fs.ensureDir(config.directories.androidDependenciesDir);
|
||||
const sdkConfig = await ensureAndroidSDKIsPresent();
|
||||
const ndkConfig = await ensureAndroidNDKIsPresent();
|
||||
_setEnvVars({ ...sdkConfig.envVars, ...ndkConfig.envVars });
|
||||
_alterPath([...sdkConfig.path, ...sdkConfig.path]);
|
||||
}
|
||||
|
||||
function formatShellAppTarballUriPath(sdkMajorVersion: string) {
|
||||
return path.join(config.directories.shellTarballsDir, PLATFORMS.ANDROID, `sdk${sdkMajorVersion}`);
|
||||
}
|
||||
|
||||
async function _shellAppPostDownloadAction(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'));
|
||||
|
||||
// TODO: remove following lines after upgrading android shell app
|
||||
const toolsPublicDir = path.join(workingdir, 'tools-public');
|
||||
await _installNodeModules(toolsPublicDir);
|
||||
}
|
||||
|
||||
async function _installNodeModules(cwd: string) {
|
||||
l.info(`installing dependencies in ${cwd} directory...`);
|
||||
const command = await _shouldUseYarnOrNpm();
|
||||
await ExponentTools.spawnAsyncThrowError(command, ['install'], {
|
||||
pipeToLogger: true,
|
||||
loggerFields: LOGGER_FIELDS,
|
||||
cwd,
|
||||
});
|
||||
l.info('dependencies installed!');
|
||||
}
|
||||
|
||||
async function _shouldUseYarnOrNpm() {
|
||||
try {
|
||||
await which('yarn');
|
||||
return 'yarn';
|
||||
} catch (err) {
|
||||
return 'npm';
|
||||
}
|
||||
}
|
||||
|
||||
function _setEnvVars(envVars: object) {
|
||||
Object
|
||||
.entries(envVars)
|
||||
.forEach(([key, value]) => process.env[key] = value);
|
||||
}
|
||||
|
||||
function _alterPath(newPaths: string[]) {
|
||||
const currentPaths = process.env.PATH ? process.env.PATH.split(':') : [];
|
||||
const paths = [...newPaths, ...currentPaths];
|
||||
process.env.PATH = paths.join(':');
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
import os from 'os';
|
||||
import path from 'path';
|
||||
|
||||
import decompress from 'decompress';
|
||||
import fs from 'fs-extra';
|
||||
|
||||
import { formatArtifactDownloadPath } from 'turtle/bin/setup/utils/common';
|
||||
import download from 'turtle/bin/setup/utils/downloader';
|
||||
import config from 'turtle/config';
|
||||
import logger from 'turtle/logger';
|
||||
|
||||
const ANDROID_NDK_URL = `https://dl.google.com/android/repository/android-ndk-r10e-${os.platform()}-x86_64.zip`;
|
||||
|
||||
const LOGGER_FIELDS = { buildPhase: 'setting up environment' };
|
||||
const l = logger.withFields(LOGGER_FIELDS);
|
||||
|
||||
export default async function ensureAndroidNDKIsPresent() {
|
||||
const androidNdkDir = path.join(config.directories.androidDependenciesDir, 'ndk');
|
||||
if (!(await fs.pathExists(androidNdkDir))) {
|
||||
await fs.ensureDir(androidNdkDir);
|
||||
const androidNdkDownloadPath = formatArtifactDownloadPath(ANDROID_NDK_URL);
|
||||
|
||||
try {
|
||||
l.info('Downloading Android NDK');
|
||||
await fs.ensureDir(config.directories.artifactsDir);
|
||||
await download(ANDROID_NDK_URL, androidNdkDownloadPath);
|
||||
l.info('Decompressing Android NDK');
|
||||
await decompress(androidNdkDownloadPath, androidNdkDir, { strip: 1 });
|
||||
l.info('Android NDK installed successfully');
|
||||
} catch (err) {
|
||||
await fs.remove(androidNdkDir);
|
||||
throw err;
|
||||
} finally {
|
||||
await fs.remove(androidNdkDownloadPath);
|
||||
}
|
||||
}
|
||||
return {
|
||||
envVars: _createEnvVars(androidNdkDir),
|
||||
path: _createPaths(androidNdkDir),
|
||||
};
|
||||
}
|
||||
|
||||
function _createEnvVars(androidNdkDir: string) {
|
||||
return {
|
||||
ANDROID_NDK: androidNdkDir,
|
||||
};
|
||||
}
|
||||
|
||||
function _createPaths(androidNdkDir: string) {
|
||||
return [androidNdkDir];
|
||||
}
|
||||
@@ -1,78 +0,0 @@
|
||||
import os from 'os';
|
||||
import path from 'path';
|
||||
|
||||
import decompress from 'decompress';
|
||||
import fs from 'fs-extra';
|
||||
import { ExponentTools } from 'xdl';
|
||||
|
||||
import { formatArtifactDownloadPath } from 'turtle/bin/setup/utils/common';
|
||||
import download from 'turtle/bin/setup/utils/downloader';
|
||||
import config from 'turtle/config';
|
||||
import logger from 'turtle/logger';
|
||||
|
||||
const ANDROID_SDK_URL = 'https://dl.google.com/android/repository/sdk-tools-linux-3859397.zip';
|
||||
|
||||
const LOGGER_FIELDS = { buildPhase: 'setting up environment' };
|
||||
const l = logger.withFields(LOGGER_FIELDS);
|
||||
|
||||
export default async function ensureAndroidSDKIsPresent() {
|
||||
const androidSdkDir = path.join(config.directories.androidDependenciesDir, 'sdk');
|
||||
if (!(await fs.pathExists(androidSdkDir))) {
|
||||
await fs.ensureDir(androidSdkDir);
|
||||
const androidSdkDownloadPath = formatArtifactDownloadPath(ANDROID_SDK_URL);
|
||||
|
||||
try {
|
||||
l.info('Downloading Android SDK');
|
||||
await fs.ensureDir(config.directories.artifactsDir);
|
||||
await download(ANDROID_SDK_URL, androidSdkDownloadPath);
|
||||
await fs.ensureDir(androidSdkDir);
|
||||
l.info('Decompressing Android SDK');
|
||||
await decompress(androidSdkDownloadPath, androidSdkDir);
|
||||
await fs.remove(androidSdkDownloadPath);
|
||||
l.info('Configuring Android SDK, this may take a while');
|
||||
await _configureSdk(androidSdkDir);
|
||||
l.info('Android SDK installed and configured successfully');
|
||||
} catch (err) {
|
||||
await fs.remove(androidSdkDir);
|
||||
throw err;
|
||||
} finally {
|
||||
await fs.remove(androidSdkDownloadPath);
|
||||
}
|
||||
}
|
||||
return {
|
||||
envVars: _createEnvVars(androidSdkDir),
|
||||
path: _createPaths(androidSdkDir),
|
||||
};
|
||||
}
|
||||
|
||||
async function _configureSdk(androidSdkDir: string) {
|
||||
// prevents warnings about missing repo config
|
||||
const androidRepositoriesCfgPath = path.join(os.homedir(), '.android/repositories.cfg');
|
||||
await fs.ensureFile(androidRepositoriesCfgPath);
|
||||
await ExponentTools.spawnAsyncThrowError('./configureAndroidSdk.sh', [], {
|
||||
pipeToLogger: true,
|
||||
loggerFields: LOGGER_FIELDS,
|
||||
cwd: path.join(config.directories.rootDir, 'scripts/cli'),
|
||||
env: {
|
||||
...process.env,
|
||||
ANDROID_HOME: androidSdkDir,
|
||||
ANDROID_SDK: androidSdkDir,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function _createEnvVars(androidSdkDir: string) {
|
||||
return {
|
||||
ANDROID_HOME: androidSdkDir,
|
||||
ANDROID_SDK: androidSdkDir,
|
||||
};
|
||||
}
|
||||
|
||||
function _createPaths(androidSdkDir: string) {
|
||||
return [
|
||||
path.join(androidSdkDir, 'platform-tools'),
|
||||
path.join(androidSdkDir, 'tools'),
|
||||
path.join(androidSdkDir, 'tools/bin'),
|
||||
path.join(androidSdkDir, 'build-tools/25.0.0'),
|
||||
];
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
import path from 'path';
|
||||
|
||||
import { ExponentTools } from 'xdl';
|
||||
|
||||
import { checkSystem, ensureShellAppIsPresent } from 'turtle/bin/setup/utils/common';
|
||||
import { IToolDefinition } from 'turtle/bin/setup/utils/toolsDetector';
|
||||
import { formatShellAppDirectory } from 'turtle/builders/utils/ios/workingdir';
|
||||
import config from 'turtle/config';
|
||||
import { PLATFORMS } from 'turtle/constants';
|
||||
|
||||
const REQUIRED_TOOLS: IToolDefinition[] = [
|
||||
{
|
||||
command: 'bash',
|
||||
missingDescription: 'Please install bash',
|
||||
},
|
||||
{
|
||||
command: 'fastlane',
|
||||
missingDescription: 'Please check https://docs.fastlane.tools/getting-started/ios/setup/',
|
||||
},
|
||||
{
|
||||
command: 'xcode',
|
||||
missingDescription: 'Please ensure xcode is properly installed',
|
||||
testFn: async () => {
|
||||
const { status, stdout } = await ExponentTools.spawnAsyncThrowError(
|
||||
'xcodebuild',
|
||||
['-version'],
|
||||
{ stdio: 'pipe' },
|
||||
);
|
||||
if (stdout.match(/requires xcode/i)) {
|
||||
return false;
|
||||
}
|
||||
return status === 0;
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
export default async function setup(sdkVersion?: string) {
|
||||
await checkSystem(REQUIRED_TOOLS);
|
||||
if (sdkVersion) {
|
||||
await ensureShellAppIsPresent(sdkVersion, {
|
||||
formatShellAppDirectory,
|
||||
formatShellAppTarballUriPath,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function formatShellAppTarballUriPath(sdkMajorVersion: string) {
|
||||
return path.join(config.directories.shellTarballsDir, PLATFORMS.IOS, `sdk${sdkMajorVersion}`);
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
import isSemver from 'is-semver';
|
||||
|
||||
import setupAndroid from 'turtle/bin/setup/android';
|
||||
import setupIos from 'turtle/bin/setup/ios';
|
||||
import { PLATFORMS } from 'turtle/constants';
|
||||
|
||||
export default async function setup(platform: string, sdkVersion?: string) {
|
||||
if (sdkVersion && !isSemver(sdkVersion)) {
|
||||
throw new Error('SDK version is not valid.');
|
||||
}
|
||||
|
||||
if (platform === PLATFORMS.IOS) {
|
||||
return await setupIos(sdkVersion);
|
||||
} else if (platform === PLATFORMS.ANDROID) {
|
||||
return await setupAndroid(sdkVersion);
|
||||
} else {
|
||||
throw new Error('This should never happen :(');
|
||||
}
|
||||
}
|
||||
@@ -1,66 +0,0 @@
|
||||
import path from 'path';
|
||||
|
||||
import fs from 'fs-extra';
|
||||
import tar from 'tar';
|
||||
import { ExponentTools } from 'xdl';
|
||||
|
||||
import download from 'turtle/bin/setup/utils/downloader';
|
||||
import { ensureToolsAreInstalled, IToolDefinition } from 'turtle/bin/setup/utils/toolsDetector';
|
||||
import config from 'turtle/config';
|
||||
import logger from 'turtle/logger';
|
||||
|
||||
const l = logger.withFields({ buildPhase: 'setting up environment' });
|
||||
|
||||
interface IShellAppFormaters {
|
||||
formatShellAppDirectory: (sdkVersion: string) => string;
|
||||
formatShellAppTarballUriPath: (sdkMajorVersion: string) => string;
|
||||
}
|
||||
|
||||
type PostDownloadAction = (workingdir: string) => void;
|
||||
|
||||
export async function checkSystem(requiredTools: IToolDefinition[]) {
|
||||
await ensureToolsAreInstalled(requiredTools);
|
||||
}
|
||||
|
||||
export async function ensureShellAppIsPresent(
|
||||
sdkVersion: string,
|
||||
formatters: IShellAppFormaters,
|
||||
postDownloadAction?: PostDownloadAction,
|
||||
) {
|
||||
const workingdir = formatters.formatShellAppDirectory(sdkVersion);
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
async function _downloadShellApp(sdkVersion: string, targetDirectory: string, formatters: IShellAppFormaters) {
|
||||
const shellAppTarballS3Uri = await _readShellAppTarballS3Uri(sdkVersion, formatters);
|
||||
const tarballDownloadTargetPath = formatArtifactDownloadPath(shellAppTarballS3Uri);
|
||||
await fs.ensureDir(config.directories.artifactsDir);
|
||||
await download(shellAppTarballS3Uri, tarballDownloadTargetPath);
|
||||
l.info('shell app has been downloaded');
|
||||
await fs.ensureDir(targetDirectory);
|
||||
l.info('extracting shell app (this may take a while)...');
|
||||
await tar.x({
|
||||
file: tarballDownloadTargetPath,
|
||||
C: targetDirectory,
|
||||
});
|
||||
l.info('shell app extracted');
|
||||
}
|
||||
|
||||
export function formatArtifactDownloadPath(uri: string) {
|
||||
const { base } = path.parse(uri);
|
||||
return path.join(config.directories.artifactsDir, base);
|
||||
}
|
||||
|
||||
async function _readShellAppTarballS3Uri(sdkVersion: string, formatters: IShellAppFormaters) {
|
||||
const sdkMajorVersion = ExponentTools.parseSdkMajorVersion(sdkVersion);
|
||||
const filePath = formatters.formatShellAppTarballUriPath(sdkMajorVersion);
|
||||
const data = await fs.readFile(filePath, 'utf8');
|
||||
return data.trim();
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
import fs from 'fs';
|
||||
import { URL } from 'url';
|
||||
|
||||
import ProgressBar from 'progress';
|
||||
import request from 'request';
|
||||
import requestProgress from 'request-progress';
|
||||
|
||||
const S3_PROTOCOL = 's3:';
|
||||
const PROGRESS_BAR_CONFIG = {
|
||||
TEMPLATE: 'downloading [:bar] :percent :etas',
|
||||
BAR: {
|
||||
COMPLETE_CHAR: '=',
|
||||
INCOMPLETE_CHAR: ' ',
|
||||
WIDTH: 20,
|
||||
},
|
||||
};
|
||||
|
||||
export default async function download(url: string, destPath: string) {
|
||||
const httpUrl = url.startsWith(S3_PROTOCOL) ? convertS3ToHttpsUrl(url) : url;
|
||||
let bar: ProgressBar;
|
||||
|
||||
return new Promise((res, rej) => {
|
||||
requestProgress(request(httpUrl))
|
||||
.on('progress', (state: any) => {
|
||||
if (!bar) {
|
||||
bar = new ProgressBar(PROGRESS_BAR_CONFIG.TEMPLATE, {
|
||||
complete: PROGRESS_BAR_CONFIG.BAR.COMPLETE_CHAR,
|
||||
incomplete: PROGRESS_BAR_CONFIG.BAR.INCOMPLETE_CHAR,
|
||||
width: PROGRESS_BAR_CONFIG.BAR.WIDTH,
|
||||
total: state.size.total,
|
||||
});
|
||||
}
|
||||
bar.tick(state.size.transferred - (bar as any).curr);
|
||||
})
|
||||
.on('error', (err: Error) => rej(err))
|
||||
.on('end', () => res())
|
||||
.pipe(fs.createWriteStream(destPath));
|
||||
});
|
||||
}
|
||||
|
||||
function convertS3ToHttpsUrl(s3UrlRaw: string) {
|
||||
const { host: s3Bucket, pathname: s3Path, protocol } = new URL(s3UrlRaw);
|
||||
if (protocol !== S3_PROTOCOL) {
|
||||
throw new Error('Wrong S3 URL provided');
|
||||
}
|
||||
return `https://${s3Bucket}.s3.amazonaws.com${s3Path}`;
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
import util from 'util';
|
||||
import _which from 'which';
|
||||
|
||||
import logger from 'turtle/logger';
|
||||
|
||||
const which = util.promisify(_which);
|
||||
const l = logger.withFields({ buildPhase: 'setting up environment' });
|
||||
|
||||
export interface IToolDefinition {
|
||||
command: string;
|
||||
missingDescription: string;
|
||||
testFn?: () => Promise<boolean>;
|
||||
}
|
||||
|
||||
export async function ensureToolsAreInstalled(tools: IToolDefinition[]) {
|
||||
let isAnyToolMissing = false;
|
||||
for (const { command, missingDescription, testFn } of tools) {
|
||||
try {
|
||||
if (testFn) {
|
||||
if (!await testFn()) {
|
||||
throw new Error('Required tool doesn\'t exist');
|
||||
}
|
||||
} else {
|
||||
await which(command);
|
||||
}
|
||||
} catch (err) {
|
||||
isAnyToolMissing = true;
|
||||
if (!testFn) {
|
||||
l.error(`${command} is missing in your $PATH`);
|
||||
}
|
||||
l.error(missingDescription);
|
||||
}
|
||||
}
|
||||
if (isAnyToolMissing) {
|
||||
throw new Error('Tools required to perform standalone app build are missing. Please install them.');
|
||||
}
|
||||
}
|
||||
@@ -1,85 +0,0 @@
|
||||
import _ from 'lodash';
|
||||
|
||||
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 logger from 'turtle/logger';
|
||||
import { sanitizeJob } from 'turtle/validator';
|
||||
|
||||
export function createBuilderAction({
|
||||
program,
|
||||
command,
|
||||
prepareCredentials,
|
||||
buildJobObject,
|
||||
builder,
|
||||
platform,
|
||||
os,
|
||||
}: any) {
|
||||
return async (projectDirArg: string, cmd: any) => {
|
||||
try {
|
||||
const osList = [];
|
||||
if (Array.isArray(os)) {
|
||||
osList.push(...os);
|
||||
} else {
|
||||
osList.push(os);
|
||||
}
|
||||
if (osList.length && !osList.includes(process.platform)) {
|
||||
throw new Error('We don\'t support running standalone app builds for this platform on your operating system');
|
||||
}
|
||||
|
||||
if (cmd.buildDir && cmd.output) {
|
||||
throw new ErrorWithCommandHelp(
|
||||
'You can\'t provide both --build-dir and --output parameters, please choose one',
|
||||
);
|
||||
}
|
||||
|
||||
const userData = {
|
||||
username: cmd.username || process.env.EXPO_USERNAME,
|
||||
password: cmd.password || process.env.EXPO_PASSWORD,
|
||||
};
|
||||
|
||||
if (userData.username || userData.password) {
|
||||
if (userData.username && userData.password) {
|
||||
await UserUtils.ensureUserLoggedIn(userData);
|
||||
} else {
|
||||
throw new ErrorWithCommandHelp('You must provide both Expo username and password');
|
||||
}
|
||||
} else if (!cmd.publicUrl) {
|
||||
throw new ErrorWithCommandHelp(
|
||||
'You must provide your Expo username and password unless you specify --public-url to your project manifest.',
|
||||
);
|
||||
}
|
||||
|
||||
const args = {
|
||||
releaseChannel: cmd.releaseChannel || 'default',
|
||||
buildType: cmd.type,
|
||||
username: userData.username || 'anonymous',
|
||||
projectDir: ProjectUtils.resolveAbsoluteDir(projectDirArg),
|
||||
publicUrl: cmd.publicUrl,
|
||||
};
|
||||
|
||||
const appJSON = await ProjectUtils.loadAppJSON(projectDirArg);
|
||||
const sdkVersion = _.get(appJSON, 'expo.sdkVersion');
|
||||
await setup(platform, sdkVersion);
|
||||
const credentials = await prepareCredentials(cmd);
|
||||
const rawJob = {
|
||||
...buildJobObject(appJSON, args, credentials),
|
||||
...cmd.buildDir && { fakeUploadDir: ProjectUtils.resolveAbsoluteDir(cmd.buildDir) },
|
||||
...cmd.output && { fakeUploadBuildPath: ProjectUtils.resolveAbsoluteDir(cmd.output) },
|
||||
};
|
||||
const job = await sanitizeJob(rawJob);
|
||||
await builder(job);
|
||||
} catch (err) {
|
||||
logger.error(`Failed to build standalone app: ${err.message}`);
|
||||
logger.error(err.stack);
|
||||
if (err instanceof ErrorWithCommandHelp) {
|
||||
command.help();
|
||||
} else if (err instanceof ErrorWithProgramHelp) {
|
||||
program.help();
|
||||
}
|
||||
process.exit(1);
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
import fs from 'fs-extra';
|
||||
import path from 'path';
|
||||
|
||||
export async function loadAppJSON(projectDirArg: string) {
|
||||
const projectDir = resolveAbsoluteDir(projectDirArg);
|
||||
const appJSONPath = path.join(projectDir, 'app.json');
|
||||
const appJSONExists = await fs.pathExists(appJSONPath);
|
||||
if (!appJSONExists) {
|
||||
throw new Error(`Couldn't find app.json in ${projectDir} directory.`);
|
||||
} else {
|
||||
return require(appJSONPath);
|
||||
}
|
||||
}
|
||||
|
||||
export const resolveAbsoluteDir = (dir: string) => {
|
||||
if (dir && path.isAbsolute(dir)) {
|
||||
return dir;
|
||||
} else {
|
||||
const cwd = process.env.INIT_CWD || process.cwd();
|
||||
return dir ? path.join(cwd, dir) : cwd;
|
||||
}
|
||||
};
|
||||
@@ -1,23 +0,0 @@
|
||||
import { User as UserManager } from 'xdl';
|
||||
|
||||
import { ErrorWithProgramHelp } from 'turtle/bin/commands/ErrorWithProgramHelp';
|
||||
|
||||
interface IUserData {
|
||||
username?: string;
|
||||
password?: string;
|
||||
}
|
||||
|
||||
export async function ensureUserLoggedIn(userData: IUserData) {
|
||||
const currentUser = await UserManager.getCurrentUserAsync();
|
||||
if (currentUser) {
|
||||
return currentUser;
|
||||
} else if (!userData.username || !userData.password) {
|
||||
throw new ErrorWithProgramHelp('Please provide username and password');
|
||||
} else {
|
||||
try {
|
||||
return await UserManager.loginAsync('user-pass', userData);
|
||||
} catch (err) {
|
||||
throw new Error('Failed to log in with provided username and password');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,91 +0,0 @@
|
||||
import path from 'path';
|
||||
|
||||
import fs from 'fs-extra';
|
||||
import _ from 'lodash';
|
||||
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 { uploadBuildToS3 } from 'turtle/builders/utils/uploader';
|
||||
import config from 'turtle/config';
|
||||
import { IAndroidCredentials, IJob, IJobResult } from 'turtle/job';
|
||||
import logger from 'turtle/logger';
|
||||
|
||||
export default async function buildAndroid(jobData: IJob): Promise<IJobResult> {
|
||||
const credentials = await getOrCreateCredentials(jobData);
|
||||
const apkFilePath = await runShellAppBuilder(jobData, credentials);
|
||||
const randomHex = uuidv4().replace(/-/g, '');
|
||||
const s3Filename = `${jobData.experienceName}-${randomHex}-signed.apk`;
|
||||
const s3FileKey = `android/${s3Filename}`;
|
||||
const fakeUploadFilename = s3Filename.replace('/', '\\');
|
||||
const artifactUrl = await uploadBuildToS3({
|
||||
uploadPath: apkFilePath,
|
||||
s3FileKey,
|
||||
...config.builder.fakeUpload && {
|
||||
fakeUploadBuildPath:
|
||||
jobData.fakeUploadBuildPath
|
||||
? jobData.fakeUploadBuildPath
|
||||
: path.join(jobData.fakeUploadDir || config.directories.fakeUploadDir, fakeUploadFilename),
|
||||
},
|
||||
});
|
||||
|
||||
return { artifactUrl };
|
||||
}
|
||||
|
||||
async function runShellAppBuilder(
|
||||
jobData: IJob,
|
||||
credentials: IAndroidCredentials,
|
||||
): Promise<string> {
|
||||
const { temporaryFilesRoot } = config.directories;
|
||||
await fs.ensureDir(temporaryFilesRoot);
|
||||
const tempShellAppConfigPath = path.join(temporaryFilesRoot, `app-config-${jobData.id}.json`);
|
||||
const tempKeystorePath = path.join(temporaryFilesRoot, `keystore-${jobData.id}.jks`);
|
||||
const configJSON = JSON.stringify(jobData.config);
|
||||
await fs.writeFile(tempShellAppConfigPath, configJSON, { mode: 0o644 });
|
||||
await fs.writeFile(tempKeystorePath, Buffer.from(credentials.keystore, 'base64'), {
|
||||
mode: 0o600,
|
||||
});
|
||||
|
||||
logger.info({ buildPhase: 'starting builder' }, 'Starting build process');
|
||||
|
||||
logger.info(
|
||||
{ buildPhase: 'icons setup' },
|
||||
'ImageUtils: setting image functions to alternative sharp implementations',
|
||||
);
|
||||
ImageUtils.setResizeImageFunction(imageHelpers.resizeIconWithSharpAsync);
|
||||
ImageUtils.setGetImageDimensionsFunction(imageHelpers.getImageDimensionsWithSharpAsync);
|
||||
|
||||
const outputFilePath = path.join(temporaryFilesRoot, `shell-signed-${jobData.id}.apk`);
|
||||
|
||||
const { manifest: { sdkVersion: sdkVersionFromManifest = null } = {}, sdkVersion: sdkVersionFromJob } = jobData;
|
||||
const sdkVersion = sdkVersionFromJob || sdkVersionFromManifest;
|
||||
const workingDir = formatShellAppDirectory(sdkVersion);
|
||||
|
||||
try {
|
||||
await AndroidShellApp.createAndroidShellAppAsync({
|
||||
url: commonUtils.getExperienceUrl(jobData),
|
||||
sdkVersion: _.get(jobData, 'manifest.sdkVersion') || jobData.sdkVersion,
|
||||
keystore: tempKeystorePath,
|
||||
alias: credentials.keystoreAlias,
|
||||
keystorePassword: credentials.keystorePassword,
|
||||
keyPassword: credentials.keyPassword,
|
||||
privateConfigFile: tempShellAppConfigPath,
|
||||
releaseChannel: jobData.config.releaseChannel,
|
||||
workingDir,
|
||||
outputFile: outputFilePath,
|
||||
});
|
||||
} catch (err) {
|
||||
commonUtils.logErrorOnce(err);
|
||||
throw err;
|
||||
} finally {
|
||||
if (!config.builder.skipCleanup) {
|
||||
await fs.unlink(tempShellAppConfigPath);
|
||||
await fs.unlink(tempKeystorePath);
|
||||
}
|
||||
}
|
||||
|
||||
return outputFilePath;
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
import buildAndroid from './android';
|
||||
import buildIOS from './ios';
|
||||
|
||||
export default {
|
||||
ios: buildIOS,
|
||||
android: buildAndroid,
|
||||
};
|
||||
@@ -1,62 +0,0 @@
|
||||
import _ from 'lodash';
|
||||
import { IosIPABuilder as createIPABuilder } from 'xdl';
|
||||
|
||||
import { IContext } from 'turtle/builders/ios/context';
|
||||
import { logErrorOnce } from 'turtle/builders/utils/common';
|
||||
import * as fileUtils from 'turtle/builders/utils/file';
|
||||
import * as keychain from 'turtle/builders/utils/ios/keychain';
|
||||
import runShellAppBuilder from 'turtle/builders/utils/ios/shellAppBuilder';
|
||||
import { IJob } from 'turtle/job';
|
||||
import logger from 'turtle/logger/index';
|
||||
|
||||
export default async function buildArchive(ctx: IContext, job: IJob) {
|
||||
let keychainInfo;
|
||||
try {
|
||||
keychainInfo = await keychain.create(ctx);
|
||||
const { credentials } = job;
|
||||
const { certP12, certPassword } = credentials as { certP12: string; certPassword: string };
|
||||
await keychain.importCert(ctx, { keychainPath: keychainInfo.path, certP12, certPassword });
|
||||
const manifest = await runShellAppBuilder(ctx, job);
|
||||
await buildAndSignIPA(ctx, job, keychainInfo.path, manifest);
|
||||
} catch (err) {
|
||||
logErrorOnce(err);
|
||||
throw err;
|
||||
} finally {
|
||||
if (keychainInfo) {
|
||||
await keychain.remove(ctx, keychainInfo.path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function buildAndSignIPA(ctx: IContext, job: IJob, keychainPath: string, manifest: any) {
|
||||
const l = logger.withFields({ buildPhase: 'building and signing IPA' });
|
||||
l.info('building and signing IPA');
|
||||
|
||||
const {
|
||||
credentials: { provisioningProfile, certPassword, teamId, password },
|
||||
config: { bundleIdentifier: bundleIdentifierFromConfig },
|
||||
} = job;
|
||||
|
||||
const bundleIdentifierFromManifest = _.get(manifest, 'ios.bundleIdentifier');
|
||||
const bundleIdentifier = bundleIdentifierFromConfig || bundleIdentifierFromManifest;
|
||||
|
||||
const { provisioningProfilePath } = ctx;
|
||||
await fileUtils.writeBase64ToBinaryFile(provisioningProfilePath, provisioningProfile as string);
|
||||
l.info('saved provisioning profile to temporary path');
|
||||
|
||||
const ipaBuilder = createIPABuilder({
|
||||
keychainPath,
|
||||
provisioningProfilePath,
|
||||
appUUID: ctx.appUUID,
|
||||
certPath: ctx.tempCertPath,
|
||||
certPassword,
|
||||
teamID: teamId,
|
||||
password,
|
||||
bundleIdentifier,
|
||||
workingDir: ctx.workingDir,
|
||||
manifest,
|
||||
});
|
||||
await ipaBuilder.build();
|
||||
|
||||
l.info(`done building and signing IPA`);
|
||||
}
|
||||
@@ -1,91 +0,0 @@
|
||||
import * as path from 'path';
|
||||
|
||||
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 { IJob } from 'turtle/job';
|
||||
|
||||
export interface IContext {
|
||||
appDir: string;
|
||||
appUUID: string;
|
||||
archiveDir: string;
|
||||
baseArchiveDir: string;
|
||||
buildDir: string;
|
||||
fakeUploadBuildPath?: string;
|
||||
outputPath: string;
|
||||
provisioningProfilePath: string;
|
||||
provisioningProfileDir: string;
|
||||
s3FileKey?: string;
|
||||
tempCertPath: string;
|
||||
uploadPath: string;
|
||||
workingDir: string;
|
||||
}
|
||||
|
||||
const { DEFAULT_EXPOKIT_WORKSPACE_NAME } = IosShellApp;
|
||||
const { BUILD_TYPES } = IOS;
|
||||
|
||||
export function createBuilderContext(job: IJob): IContext {
|
||||
const { join } = path;
|
||||
const appUUID = uuidv4();
|
||||
const {
|
||||
manifest: {
|
||||
sdkVersion: sdkVersionFromManifest = null,
|
||||
} = {},
|
||||
sdkVersion: sdkVersionFromJob,
|
||||
config: { buildType },
|
||||
} = job;
|
||||
|
||||
const sdkVersion = sdkVersionFromJob || sdkVersionFromManifest;
|
||||
const workingDir = formatShellAppDirectory(sdkVersion);
|
||||
|
||||
const context: any = {
|
||||
appDir: join(config.directories.temporaryFilesRoot, appUUID),
|
||||
appUUID,
|
||||
workingDir,
|
||||
};
|
||||
context.buildDir = join(context.appDir, 'build');
|
||||
context.provisioningProfileDir = join(context.appDir, 'provisioning');
|
||||
context.provisioningProfilePath = join(
|
||||
context.provisioningProfileDir,
|
||||
`${appUUID}.mobileprovision`,
|
||||
);
|
||||
context.tempCertPath = join(context.appDir, 'cert.p12');
|
||||
context.baseArchiveDir = join(context.appDir, 'archive');
|
||||
|
||||
if (buildType === BUILD_TYPES.SIMULATOR) {
|
||||
context.outputPath = join(context.appDir, 'archive.tar.gz');
|
||||
context.uploadPath = join(context.appDir, 'archive.tar.gz');
|
||||
context.archiveDir = join(
|
||||
context.baseArchiveDir,
|
||||
'Release',
|
||||
`${DEFAULT_EXPOKIT_WORKSPACE_NAME}.app`,
|
||||
);
|
||||
} else if (buildType === BUILD_TYPES.ARCHIVE) {
|
||||
context.outputPath = join(context.appDir, 'archive.xcarchive');
|
||||
context.uploadPath = join(context.buildDir, 'archive.ipa');
|
||||
context.archiveDir = join(
|
||||
context.baseArchiveDir,
|
||||
'Release',
|
||||
`${DEFAULT_EXPOKIT_WORKSPACE_NAME}.xcarchive`,
|
||||
'Products',
|
||||
'Applications',
|
||||
`${DEFAULT_EXPOKIT_WORKSPACE_NAME}.app`,
|
||||
);
|
||||
}
|
||||
|
||||
const s3FileExtension = buildType === BUILD_TYPES.SIMULATOR ? 'tar.gz' : 'ipa';
|
||||
const s3Filename = `${job.experienceName}-${appUUID}-${buildType}.${s3FileExtension}`;
|
||||
if (config.builder.fakeUpload) {
|
||||
const fakeUploadFilename = s3Filename.replace('/', '\\');
|
||||
context.fakeUploadBuildPath = job.fakeUploadBuildPath
|
||||
? job.fakeUploadBuildPath
|
||||
: join(job.fakeUploadDir || config.directories.fakeUploadDir, fakeUploadFilename);
|
||||
} else {
|
||||
context.s3FileKey = join(PLATFORMS.IOS, s3Filename);
|
||||
}
|
||||
|
||||
return context;
|
||||
}
|
||||
@@ -1,84 +0,0 @@
|
||||
import path from 'path';
|
||||
|
||||
import fs from 'fs-extra';
|
||||
import pick from 'lodash/pick';
|
||||
import { ExponentTools } from 'xdl';
|
||||
|
||||
import buildArchive from 'turtle/builders/ios/archive';
|
||||
import { createBuilderContext, IContext } from 'turtle/builders/ios/context';
|
||||
import buildSimulator from 'turtle/builders/ios/simulator';
|
||||
import { logErrorOnce } from 'turtle/builders/utils/common';
|
||||
import { uploadBuildToS3 } from 'turtle/builders/utils/uploader';
|
||||
import config from 'turtle/config';
|
||||
import { IOS, PLATFORMS } from 'turtle/constants/index';
|
||||
import { IJob, IJobResult } from 'turtle/job';
|
||||
|
||||
const { BUILD_TYPES } = IOS;
|
||||
|
||||
let SUPPORTED_SDK_VERSIONS: number[];
|
||||
|
||||
export default async function iosBuilder(job: IJob): Promise<IJobResult> {
|
||||
await ensureCanBuildSdkVersion(job);
|
||||
|
||||
const ctx = createBuilderContext(job);
|
||||
|
||||
try {
|
||||
await initBuilder(ctx);
|
||||
|
||||
const { buildType } = job.config;
|
||||
if (buildType === BUILD_TYPES.SIMULATOR) {
|
||||
await buildSimulator(ctx, job);
|
||||
} else if (buildType === BUILD_TYPES.ARCHIVE) {
|
||||
await buildArchive(ctx, job);
|
||||
} else {
|
||||
throw new Error(`Unsupported iOS build type: ${buildType}`);
|
||||
}
|
||||
|
||||
const artifactUrl = await uploadBuildToS3(ctx);
|
||||
return { artifactUrl };
|
||||
} catch (err) {
|
||||
logErrorOnce(err);
|
||||
throw err;
|
||||
} finally {
|
||||
await cleanup(ctx);
|
||||
}
|
||||
}
|
||||
|
||||
async function initBuilder(ctx: IContext) {
|
||||
for (const dir of getTemporaryDirs(ctx)) {
|
||||
await fs.ensureDir(dir);
|
||||
await fs.chmod(dir, 0o755);
|
||||
}
|
||||
}
|
||||
|
||||
async function cleanup(ctx: IContext) {
|
||||
if (config.builder.skipCleanup) {
|
||||
return;
|
||||
}
|
||||
await Promise.all(getTemporaryDirs(ctx).map((dir: string) => fs.remove(dir)));
|
||||
}
|
||||
|
||||
const getTemporaryDirs = (ctx: IContext) =>
|
||||
Object.values(pick(ctx, ['appDir', 'provisioningProfileDir']));
|
||||
|
||||
async function ensureCanBuildSdkVersion(job: IJob) {
|
||||
if (config.builder.useLocalWorkingDir) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!SUPPORTED_SDK_VERSIONS) {
|
||||
SUPPORTED_SDK_VERSIONS = await findSupportedSdkVersions();
|
||||
}
|
||||
|
||||
const targetMajorSdkVersion = ExponentTools.parseSdkMajorVersion(job.sdkVersion || job.manifest.sdkVersion);
|
||||
if (!SUPPORTED_SDK_VERSIONS.includes(targetMajorSdkVersion)) {
|
||||
throw new Error(`Unsupported SDK Version!`);
|
||||
}
|
||||
}
|
||||
|
||||
async function findSupportedSdkVersions(): Promise<number[]> {
|
||||
const SDK_DIR_PREFIX = 'sdk';
|
||||
const files = await fs.readdir(path.join(config.directories.workingDir, PLATFORMS.IOS));
|
||||
const sdks = files.filter((file) => file.startsWith(SDK_DIR_PREFIX));
|
||||
return sdks.map((sdk) => parseInt(sdk.substr(SDK_DIR_PREFIX.length), 10));
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
import { IContext } from 'turtle/builders/ios/context';
|
||||
import runShellAppBuilder from 'turtle/builders/utils/ios/shellAppBuilder';
|
||||
import { IJob } from 'turtle/job';
|
||||
import logger from 'turtle/logger';
|
||||
|
||||
export default async function buildSimulator(ctx: IContext, job: IJob) {
|
||||
const l = logger.withFields({ buildPhase: 'running simulator builder' });
|
||||
l.info('running simulator build');
|
||||
await runShellAppBuilder(ctx, job);
|
||||
l.info('build complete');
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
import path from 'path';
|
||||
|
||||
import fs from 'fs-extra';
|
||||
import uuidv4 from 'uuid/v4';
|
||||
import { AndroidKeystore } from 'xdl';
|
||||
|
||||
import * as sqs from 'turtle/aws/sqs';
|
||||
import { UPDATE_CREDENTIALS } from 'turtle/constants/build';
|
||||
import { IAndroidCredentials, IJob } from 'turtle/job';
|
||||
import logger from 'turtle/logger';
|
||||
import { isOffline } from 'turtle/turtleContext';
|
||||
|
||||
async function getOrCreateCredentials(jobData: IJob): Promise<IAndroidCredentials> {
|
||||
if (!jobData.credentials) {
|
||||
const l = logger.withFields({ 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;
|
||||
|
||||
await AndroidKeystore.createKeystore({
|
||||
keystorePassword: credentials.keystorePassword,
|
||||
keyPassword: credentials.keyPassword,
|
||||
keystoreFilename,
|
||||
keystoreAlias: credentials.keystoreAlias,
|
||||
androidPackage,
|
||||
});
|
||||
|
||||
credentials.keystore = (await fs.readFile(keystoreFilename)).toString('base64');
|
||||
l.info('Keystore created successfully');
|
||||
if (isOffline()) {
|
||||
const projectKeystorePath = path.join(jobData.projectDir, `${jobData.experienceName}.jks`);
|
||||
await fs.copy(keystoreFilename, projectKeystorePath);
|
||||
l.info(`Saved created keystore to ${projectKeystorePath}`);
|
||||
l.info(`Keystore password: ${credentials.keystorePassword}`);
|
||||
l.info(`Keystore alias: ${credentials.keystoreAlias}`);
|
||||
l.info(`Key password: ${credentials.keyPassword}`);
|
||||
l.info(`Please keep these credentials safe`);
|
||||
} else {
|
||||
await sqs.sendMessage(jobData.id, UPDATE_CREDENTIALS, {
|
||||
...credentials,
|
||||
});
|
||||
l.info('Keystore sent to storage successfully');
|
||||
}
|
||||
await fs.unlink(keystoreFilename);
|
||||
return credentials;
|
||||
}
|
||||
|
||||
return jobData.credentials as IAndroidCredentials;
|
||||
}
|
||||
|
||||
export default getOrCreateCredentials;
|
||||
@@ -1,13 +0,0 @@
|
||||
import path from 'path';
|
||||
|
||||
import { ExponentTools } from 'xdl';
|
||||
|
||||
import config from 'turtle/config';
|
||||
import { PLATFORMS } from 'turtle/constants';
|
||||
|
||||
export function formatShellAppDirectory(sdkVersion: string) {
|
||||
const majorSdkVersion = ExponentTools.parseSdkMajorVersion(sdkVersion);
|
||||
return config.builder.useLocalWorkingDir
|
||||
? path.join(config.directories.workingDir, 'local')
|
||||
: path.join(config.directories.workingDir, PLATFORMS.ANDROID, `sdk${majorSdkVersion}`);
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
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;
|
||||
|
||||
// publicUrl is passed in if user wants to build an externally hosted app
|
||||
if (publicUrl) {
|
||||
return publicUrl;
|
||||
}
|
||||
const { protocol, hostname, port } = config.api;
|
||||
return url.format({ protocol, hostname, port, pathname: `/${experienceName}` });
|
||||
}
|
||||
|
||||
const alreadyLoggedError = Symbol('alreadyLoggedError');
|
||||
|
||||
export function logErrorOnce(err: any) {
|
||||
if (!err[alreadyLoggedError]) {
|
||||
logger.error(err.stack);
|
||||
err[alreadyLoggedError] = true;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
import fs from 'fs-extra';
|
||||
|
||||
export async function writeBase64ToBinaryFile(filename: string, base64: string) {
|
||||
const buffer = Buffer.from(base64, 'base64');
|
||||
await fs.writeFile(filename, buffer);
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
import path from 'path';
|
||||
|
||||
import fs from 'fs-extra';
|
||||
import sharp from 'sharp';
|
||||
|
||||
export async function resizeIconWithSharpAsync(iconSizePx: number, iconFilename: string, destinationIconPath: string) {
|
||||
const filename = path.join(destinationIconPath, iconFilename);
|
||||
|
||||
// PLEASE DON'T REMOVE THE FOLLOWING LINE (sharp caches files taking path as the cache key)
|
||||
sharp.cache(false);
|
||||
|
||||
// sharp can't have same input and output filename, so load to buffer then
|
||||
// write to disk after resize is complete
|
||||
const buffer = await sharp(filename)
|
||||
.resize(iconSizePx, iconSizePx)
|
||||
.toBuffer();
|
||||
|
||||
await fs.writeFile(filename, buffer);
|
||||
}
|
||||
|
||||
export async function getImageDimensionsWithSharpAsync(basename: string, dirname: string) {
|
||||
const filename = path.join(dirname, basename);
|
||||
|
||||
// PLEASE DON'T REMOVE THE FOLLOWING LINE (sharp caches files taking path as the cache key)
|
||||
sharp.cache(false);
|
||||
|
||||
try {
|
||||
const meta = await sharp(filename).metadata();
|
||||
return [meta.width, meta.height];
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
import { IosKeychain } from 'xdl';
|
||||
|
||||
import { IContext } from 'turtle/builders/ios/context';
|
||||
import * as fileUtils from 'turtle/builders/utils/file';
|
||||
import logger from 'turtle/logger';
|
||||
|
||||
interface IKeychain {
|
||||
path: string;
|
||||
}
|
||||
|
||||
export async function create(ctx: IContext): Promise<IKeychain> {
|
||||
const l = logger.withFields({ buildPhase: 'creating keychain' });
|
||||
try {
|
||||
l.info('creating keychain...');
|
||||
const keychainInfo = await IosKeychain.createKeychain(ctx.appUUID, false);
|
||||
l.info('done creating keychain');
|
||||
return keychainInfo;
|
||||
} catch (err) {
|
||||
l.error('unable to create keychain');
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
export async function remove(ctx: IContext, keychainPath: string) {
|
||||
const l = logger.withFields({ buildPhase: 'deleting keychain' });
|
||||
try {
|
||||
l.info('delete keychain...');
|
||||
const keychainInfo = await IosKeychain.deleteKeychain({
|
||||
path: keychainPath,
|
||||
appUUID: ctx.appUUID,
|
||||
});
|
||||
l.info('done deleting keychain');
|
||||
return keychainInfo;
|
||||
} catch (err) {
|
||||
l.error('unable to delete keychain');
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
export async function cleanUp() {
|
||||
await IosKeychain.cleanUpKeychains();
|
||||
}
|
||||
|
||||
export async function importCert(
|
||||
ctx: IContext,
|
||||
{
|
||||
keychainPath,
|
||||
certP12,
|
||||
certPassword,
|
||||
}: { keychainPath: string; certP12: string; certPassword: string },
|
||||
) {
|
||||
const l = logger.withFields({ buildPhase: 'importing certificate into keychain' });
|
||||
try {
|
||||
l.info('importing distribution certificate into keychain...');
|
||||
const { tempCertPath: certPath } = ctx;
|
||||
await fileUtils.writeBase64ToBinaryFile(certPath, certP12);
|
||||
await IosKeychain.importIntoKeychain({ keychainPath, certPath, certPassword });
|
||||
l.info('done importing distribution certificate into keychain');
|
||||
} catch (err) {
|
||||
l.error('unable to import distribution certificate into keychain');
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
import path from 'path';
|
||||
import util from 'util';
|
||||
|
||||
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 { PLATFORMS } from 'turtle/constants';
|
||||
import { IJob } from 'turtle/job';
|
||||
import logger from 'turtle/logger/index';
|
||||
|
||||
const copyAsync = util.promisify(copy);
|
||||
|
||||
export default async function runShellAppBuilder(ctx: IContext, job: IJob): Promise<any> {
|
||||
const { config: jobConfig, manifest, sdkVersion: sdkVersionFromJob } = job;
|
||||
const { buildType, releaseChannel } = jobConfig;
|
||||
const sdkVersion = _.get(manifest, 'sdkVersion', sdkVersionFromJob);
|
||||
const applicationFilesSrc = path.join(
|
||||
ctx.workingDir,
|
||||
'shellAppBase-builds',
|
||||
buildType as string,
|
||||
'**',
|
||||
'*',
|
||||
);
|
||||
await copyAsync(applicationFilesSrc, ctx.baseArchiveDir);
|
||||
|
||||
logger.info(
|
||||
{ buildPhase: 'icons setup' },
|
||||
'ImageUtils: setting image functions to alternative sharp implementations',
|
||||
);
|
||||
ImageUtils.setResizeImageFunction(imageHelpers.resizeIconWithSharpAsync);
|
||||
ImageUtils.setGetImageDimensionsFunction(imageHelpers.getImageDimensionsWithSharpAsync);
|
||||
const shellAppParams = {
|
||||
url: commonUtils.getExperienceUrl(job),
|
||||
sdkVersion,
|
||||
action: 'configure',
|
||||
type: buildType,
|
||||
releaseChannel,
|
||||
archivePath: ctx.archiveDir,
|
||||
privateConfigData: job.config,
|
||||
verbose: true,
|
||||
output: ctx.outputPath,
|
||||
expoSourcePath: path.join(ctx.workingDir, PLATFORMS.IOS),
|
||||
manifest,
|
||||
};
|
||||
|
||||
logger.info({ buildPhase: 'configuring NSBundle' }, 'configuring NSBundle...');
|
||||
return await IosShellApp.configureAndCopyArchiveAsync(shellAppParams);
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
import path from 'path';
|
||||
|
||||
import { ExponentTools } from 'xdl';
|
||||
|
||||
import config from 'turtle/config';
|
||||
import { PLATFORMS } from 'turtle/constants';
|
||||
|
||||
export function formatShellAppDirectory(sdkVersion: string) {
|
||||
const majorVersion = ExponentTools.parseSdkMajorVersion(sdkVersion);
|
||||
return config.builder.useLocalWorkingDir
|
||||
? path.join(config.directories.workingDir, 'local')
|
||||
: path.join(config.directories.workingDir, PLATFORMS.IOS, `sdk${majorVersion}`);
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
import * as fs from 'fs-extra';
|
||||
|
||||
import { uploadFile } from 'turtle/aws/s3';
|
||||
import config from 'turtle/config';
|
||||
import logger from 'turtle/logger';
|
||||
|
||||
interface IUploadCtx {
|
||||
fakeUploadBuildPath?: string;
|
||||
s3FileKey?: string;
|
||||
uploadPath: string;
|
||||
}
|
||||
|
||||
export async function uploadBuildToS3(ctx: IUploadCtx) {
|
||||
if (config.builder.fakeUpload) {
|
||||
const l = logger.withFields({ 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' });
|
||||
l.info('uploading build artifact to S3');
|
||||
const { Location: fileLocation } = await uploadFile({
|
||||
key: ctx.s3FileKey as string,
|
||||
srcPath: ctx.uploadPath,
|
||||
});
|
||||
l.info(`done uploading build artifact to S3 (${fileLocation})`);
|
||||
return fileLocation;
|
||||
}
|
||||
}
|
||||
@@ -1,91 +0,0 @@
|
||||
import os from 'os';
|
||||
import path from 'path';
|
||||
|
||||
import { PLATFORMS } from 'turtle/constants';
|
||||
import { env, envNum, envOptional, envTransform } from 'turtle/utils/env';
|
||||
|
||||
export default {
|
||||
env: env('NODE_ENV'),
|
||||
hostname: env('HOSTNAME', os.hostname()),
|
||||
deploymentEnv: env('ENVIRONMENT'),
|
||||
platform: env('PLATFORM', PLATFORMS.IOS),
|
||||
aws: {
|
||||
accessKeyId: env('AWS_ACCESS_KEY_ID'),
|
||||
secretAccessKey: env('AWS_SECRET_ACCESS_KEY'),
|
||||
},
|
||||
s3: {
|
||||
bucket: env('AWS_S3_BUCKET'),
|
||||
region: env('AWS_S3_REGION'),
|
||||
},
|
||||
sqs: {
|
||||
region: env('AWS_SQS_REGION'),
|
||||
queues: {
|
||||
normalPriority: {
|
||||
ios: env('AWS_SQS_IOS_QUEUE_URL'),
|
||||
android: env('AWS_SQS_ANDROID_QUEUE_URL'),
|
||||
},
|
||||
highPriority: {
|
||||
ios: env('AWS_SQS_IOS_PRIORITY_QUEUE_URL'),
|
||||
android: env('AWS_SQS_ANDROID_PRIORITY_QUEUE_URL'),
|
||||
},
|
||||
out: env('AWS_SQS_OUT_QUEUE_URL'),
|
||||
},
|
||||
},
|
||||
cloudwatch: {
|
||||
region: env('AWS_CLOUDWATCH_REGION'),
|
||||
disabled: envTransform('AWS_CLOUDWATCH_DISABLED', '0', (val: string) => val === '1'),
|
||||
intervalMs: envNum('AWS_CLOUDWATCH_INTERVAL_MS', 30000),
|
||||
namespace: env('AWS_CLOUDWATCH_NAMESPACE', 'Turtle'),
|
||||
},
|
||||
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'),
|
||||
},
|
||||
loggly: {
|
||||
token: env('LOGGLY_TOKEN', ''),
|
||||
subdomain: env('LOGGLY_SUBDOMAIN', ''),
|
||||
buffer: envNum('LOGGLY_BUFFER', 100),
|
||||
},
|
||||
},
|
||||
sentry: {
|
||||
dsn: env('SENTRY_DSN', ''),
|
||||
},
|
||||
api: {
|
||||
protocol: env('API_PROTOCOL'),
|
||||
hostname: env('API_HOSTNAME'),
|
||||
port: envNum('API_PORT'),
|
||||
},
|
||||
builder: {
|
||||
mode: env('TURTLE_MODE', 'online'),
|
||||
skipCleanup: envTransform(
|
||||
'TURTLE_SKIP_CLEANUP',
|
||||
'0',
|
||||
(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),
|
||||
useLocalWorkingDir: envTransform('TURTLE_USE_LOCAL_WORKING_DIR', '0', (val: string) => val === '1'),
|
||||
},
|
||||
directories: {
|
||||
rootDir: env('TURTLE_ROOT_DIR_PATH', path.join(__dirname, '..')),
|
||||
workingDir: env('TURTLE_WORKING_DIR_PATH', path.join(__dirname, '../workingdir')),
|
||||
shellTarballsDir: env('TURTLE_SHELL_TARBALLS_DIR', path.join(__dirname, '../shellTarballs')),
|
||||
artifactsDir: env('TURTLE_ARTIFACTS_DIR', path.join(__dirname, '../artifacts')),
|
||||
androidDependenciesDir: env(
|
||||
'TURTLE_ANDROID_DEPENDENCIES_DIR',
|
||||
path.join(os.homedir(), '.turtle/androidDependencies'),
|
||||
),
|
||||
tempS3LogsDir: env('TURTLE_TEMP_S3_LOGS_DIR', '/tmp/logs'),
|
||||
fakeUploadDir: envOptional('TURTLE_FAKE_UPLOAD_DIR'),
|
||||
temporaryFilesRoot: env(
|
||||
'TURTLE_TEMPORARY_FILES_DIR',
|
||||
path.join(os.platform() === 'darwin' ? '/private' : '/', 'tmp', 'turtle'),
|
||||
),
|
||||
},
|
||||
};
|
||||
@@ -1,12 +0,0 @@
|
||||
export enum JOB_STATES {
|
||||
PENDING = 'pending',
|
||||
STARTED = 'started',
|
||||
IN_PROGRESS = 'in-progress',
|
||||
FINISHED = 'finished',
|
||||
ERRORED = 'errored',
|
||||
SENT_TO_QUEUE = 'sent-to-queue',
|
||||
}
|
||||
|
||||
export const UPDATE_CREDENTIALS = 'update-credentials';
|
||||
|
||||
export type JOB_UPDATE_TYPE = JOB_STATES | 'update-credentials';
|
||||
Loaded 100 of 139 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user