Compare commits

..
Author SHA1 Message Date
Dominik Sokal 9f46861254 run lint before building turtle 2019-01-17 15:59:03 +01:00
Dominik Sokal 8a8f53101b fix tslint errors 2019-01-17 15:55:36 +01:00
Dominik Sokal 6096ac45da get rid of eslint 2019-01-17 13:56:14 +01:00
139 changed files with 16469 additions and 1 deletions

No files matched your search

+16
View File
@@ -0,0 +1,16 @@
{
"presets": [
["env", {
"targets": {
"node": "8.9"
}
}]
],
"plugins": [
["module-resolver", {
"alias": {
"turtle": "./build"
}
}]
]
}
+25
View File
@@ -0,0 +1,25 @@
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
+18
View File
@@ -0,0 +1,18 @@
#!/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
+11
View File
@@ -0,0 +1,11 @@
# 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.
+8
View File
@@ -0,0 +1,8 @@
!/src/node_modules
/build/
/workingdir/*
/artifacts/*
shell.apk
node_modules
.secrets*
+9
View File
@@ -0,0 +1,9 @@
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"
+25
View File
@@ -0,0 +1,25 @@
# 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.
+7
View File
@@ -0,0 +1,7 @@
# 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
View File
@@ -0,0 +1,124 @@
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"]
+21
View File
@@ -0,0 +1,21 @@
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.
+25 -1
View File
@@ -1 +1,25 @@
this commit is not really deployed to our builders :(
# Turtle CLI
[![License](https://img.shields.io/badge/license-MIT-green.svg?style=flat)](https://github.com/expo/turtle/blob/master/LICENSE)
[![NPM](https://img.shields.io/npm/v/turtle-cli/latest.svg)](https://rubygems.org/gems/fastlane)
[![CircleCI](https://img.shields.io/circleci/project/github/expo/turtle/master.svg)](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.
Executable
+6
View File
@@ -0,0 +1,6 @@
#!/usr/bin/env node
'use strict';
require('source-map-support').install();
require('../build/bin/env').initOfflineEnv();
require('../build/bin/index').run('turtle');
+91
View File
@@ -0,0 +1,91 @@
'use strict';
const childProcess = require('child_process');
const util = require('util');
const path = require('path');
const gulp = require('gulp');
const babel = require('gulp-babel');
const changed = require('gulp-changed');
const nodemon = require('gulp-nodemon');
const plumber = require('gulp-plumber');
const sourcemaps = require('gulp-sourcemaps');
const ts = require('gulp-typescript');
const rimraf = require('rimraf');
const rimrafAsync = util.promisify(rimraf);
const tsProject = ts.createProject('tsconfig.json');
gulp.task('clean', async () => {
console.log(`Removing build directories...`);
await rimrafAsync('build');
});
gulp.task('build:server', () => {
console.log(`Compiling the Node.js server JavaScript...`);
return tsProject
.src()
.pipe(plumber())
.pipe(changed('build'))
.pipe(sourcemaps.init())
.pipe(tsProject())
.pipe(babel())
.pipe(
sourcemaps.write('.', {
includeContent: false,
sourceRoot: '../src',
})
)
.pipe(gulp.dest('build'));
});
gulp.task('watch:source', async () => {
console.log(`Watching for changes to the Node.js server JavaScript...`);
gulp.watch('src/**/*.(t|j)s', gulp.series('build:server'));
});
gulp.task('watch:local', createWatchTask('development'));
gulp.task('watch:server', createWatchTask('production'));
function createWatchTask(nodeEnv) {
return async () => {
console.log(`Starting the Node.js server and watching for changes (NODE_ENV=${nodeEnv})...`);
// Run nodemon in a separate process to isolate its side effects on stdio streams
let nodeCmd = 'node';
if (nodeEnv === 'development') {
nodeCmd += ' --inspect=:9315';
}
nodemon({
script: './build/server.js',
execMap: {
js: nodeCmd,
},
env: {
NODE_ENV: nodeEnv,
},
watch: ['./build'],
stdout: false,
readable: false,
}).on('readable', function() {
const bunyan = childProcess.fork(
path.join('.', 'node_modules', '.bin', 'bunyan'),
['-o', 'short', '-l', 'debug', '--color'],
{ silent: true }
);
bunyan.stdout.pipe(process.stdout);
bunyan.stderr.pipe(process.stderr);
this.stdout.pipe(bunyan.stdin);
this.stderr.pipe(bunyan.stdin);
});
};
}
gulp.task(
'watch',
gulp.series('clean', 'build:server', gulp.parallel('watch:source', 'watch:local'))
);
gulp.task('default', gulp.series('watch'));
+14
View File
@@ -0,0 +1,14 @@
{
"displayName": "turtle",
"testEnvironment": "node",
"rootDir": "../src",
"moduleNameMapper": {
"^turtle/(.*)$": "<rootDir>/$1"
},
"transform": {
"^.+\\.ts$": "ts-jest"
},
"testRegex": "(test|spec)\\.ts$",
"moduleFileExtensions": ["ts", "js"],
"verbose": true
}
+14
View File
@@ -0,0 +1,14 @@
{
"displayName": "turtle",
"testEnvironment": "node",
"rootDir": "../src",
"moduleNameMapper": {
"^turtle/(.*)$": "<rootDir>/$1"
},
"transform": {
"^.+\\.ts$": "ts-jest"
},
"testMatch": [ "**/__smoke__/*.test.ts" ],
"moduleFileExtensions": ["ts", "js"],
"verbose": true
}
+14
View File
@@ -0,0 +1,14 @@
{
"displayName": "turtle",
"testEnvironment": "node",
"rootDir": "../src",
"moduleNameMapper": {
"^turtle/(.*)$": "<rootDir>/$1"
},
"transform": {
"^.+\\.ts$": "ts-jest"
},
"testMatch": [ "**/__tests__/*.ts" ],
"moduleFileExtensions": ["ts", "js"],
"verbose": true
}
+11
View File
@@ -0,0 +1,11 @@
{
"compilerOptions": {
"target": "es2017",
"allowSyntheticDefaultImports": false,
"baseUrl": "./",
"paths": {
"turtle/*": ["src/*"],
}
},
"exclude": ["node_modules", "dist", "build"]
}
+153
View File
@@ -0,0 +1,153 @@
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
View File
@@ -0,0 +1,123 @@
{
"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"
}
}
+32
View File
@@ -0,0 +1,32 @@
#!/usr/bin/env bash
set -xeo pipefail
ROOT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )"/../.. && pwd )"
ARTIFACTS_DIR="$ROOT_DIR/artifacts"
TEMP_DIR="/tmp/android-shell-app"
mkdir -p $ARTIFACTS_DIR
rm -rf $TEMP_DIR
mkdir -p $TEMP_DIR
ln -s ${EXPO_UNIVERSE_DIR}/exponent/tools-public $TEMP_DIR/tools-public
ln -s ${EXPO_UNIVERSE_DIR}/exponent/secrets $TEMP_DIR/secrets
ln -s ${EXPO_UNIVERSE_DIR}/exponent/template-files $TEMP_DIR/template-files
ln -s ${EXPO_UNIVERSE_DIR}/exponent/android $TEMP_DIR/android
ln -s ${EXPO_UNIVERSE_DIR}/exponent/package.json $TEMP_DIR/package.json
ln -s ${EXPO_UNIVERSE_DIR}/exponent/expokit-npm-package $TEMP_DIR/expokit-npm-package
ln -s ${EXPO_UNIVERSE_DIR}/exponent/packages $TEMP_DIR/packages
ln -s ${EXPO_UNIVERSE_DIR}/libraries/eslint-config-universe $TEMP_DIR/eslint-config-universe
ln -s ${EXPO_UNIVERSE_DIR}/libraries/json-file $TEMP_DIR/json-file
ln -s ${EXPO_UNIVERSE_DIR}/libraries/osascript $TEMP_DIR/osascript
ln -s ${EXPO_UNIVERSE_DIR}/libraries/schemer $TEMP_DIR/schemer
ln -s ${EXPO_UNIVERSE_DIR}/package.json $TEMP_DIR/universe-package.json
# generate dynamic macros
mkdir -p $TEMP_DIR/android/expoview/src/main/java/host/exp/exponent/generated/
cd $TEMP_DIR/tools-public
./generate-dynamic-macros-android.sh
rm -rf $TEMP_DIR/secrets
cd $TEMP_DIR; tar -czhf $ARTIFACTS_DIR/android-shell-builder.tar.gz .
rm -rf $TEMP_DIR
+10
View File
@@ -0,0 +1,10 @@
#!/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
+18
View File
@@ -0,0 +1,18 @@
#!/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
+28
View File
@@ -0,0 +1,28 @@
#!/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
+9
View File
@@ -0,0 +1,9 @@
#!/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"
+22
View File
@@ -0,0 +1,22 @@
#!/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
+23
View File
@@ -0,0 +1,23 @@
#!/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
+18
View File
@@ -0,0 +1,18 @@
#!/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
+73
View File
@@ -0,0 +1,73 @@
import { env } from 'turtle/utils/env';
import { connect, RedisClient } from 'turtle/utils/redis';
import { PLATFORMS } from 'turtle/constants';
import {
createConfigurationsKey,
createDefaultConfigurationKey,
HIGH_CONFIGURATION,
NORMAL_CONFIGURATION,
} from 'turtle/utils/priorities';
const TIMEOUT = 3000;
// Turtle before doing anything (and after every build) checks for the first config
// (an element in config table with key=i) that is not taken (config.i is null/outdated)
// or is taken by this process itself.
// If Turtle dies unexpectedly the configuration will become free after TTL expires,
// so the state (number of HP/NP turtles) should come back to normal soon.
// For example, if a configuration table looks like this:
// [ HIGH_CONFIGURATION, NORMAL_CONFIGURATION, HIGH_CONFIGURATION ]
// If there is one turtle it runs with High Priority first.
// If there are two turtles, one runs with High Priority first, the second one with Normal Priority first.
// If the first dies, and the third is started in his place, it gets 3rd config (again HP-first),
// but after some time, because of TTL for config reservations,
// they should come back to using configurations no. 1 and 2.
// If there are no more available configurations from the table, the default configuration is used.
const AVAILABLE_CONFIGURATIONS_IOS = [
HIGH_CONFIGURATION,
HIGH_CONFIGURATION,
NORMAL_CONFIGURATION,
NORMAL_CONFIGURATION,
HIGH_CONFIGURATION,
NORMAL_CONFIGURATION,
];
const AVAILABLE_CONFIGURATIONS_ANDROID = [
HIGH_CONFIGURATION,
HIGH_CONFIGURATION,
NORMAL_CONFIGURATION,
NORMAL_CONFIGURATION,
HIGH_CONFIGURATION,
HIGH_CONFIGURATION,
NORMAL_CONFIGURATION,
NORMAL_CONFIGURATION,
HIGH_CONFIGURATION,
HIGH_CONFIGURATION,
NORMAL_CONFIGURATION,
NORMAL_CONFIGURATION,
HIGH_CONFIGURATION,
HIGH_CONFIGURATION,
NORMAL_CONFIGURATION,
NORMAL_CONFIGURATION,
];
const DEFAULT_CONFIGURATION = HIGH_CONFIGURATION;
async function run() {
const redis = await connect(TIMEOUT, RedisClient.Configuration);
await redis.set(createConfigurationsKey(PLATFORMS.IOS), JSON.stringify(AVAILABLE_CONFIGURATIONS_IOS));
await redis.set(createConfigurationsKey(PLATFORMS.ANDROID), JSON.stringify(AVAILABLE_CONFIGURATIONS_ANDROID));
await redis.set(createDefaultConfigurationKey(PLATFORMS.IOS), JSON.stringify(DEFAULT_CONFIGURATION));
await redis.set(createDefaultConfigurationKey(PLATFORMS.ANDROID), JSON.stringify(DEFAULT_CONFIGURATION));
redis.disconnect();
}
run()
.then(() => console.log('All done'))
.catch(err => console.error('Error:', err));
+13
View File
@@ -0,0 +1,13 @@
#!/usr/bin/env bash
if [ -z "$1" ]; then
echo "Please provide an environment name as a first argument"
exit 1
fi
if [ -z "$2" ]; then
echo "Please provide a pod name as a second argument"
exit 1
fi
kubectl --namespace $1 exec -it $2 -- /bin/bash
+5
View File
@@ -0,0 +1,5 @@
#!/usr/bin/env bash
ENVIRONMENT=${1:-production}
kubectl get pods --namespace $ENVIRONMENT | grep "turtle-android-" | sed -e "s/ */ /g" | cut -d" " -f1
+5
View File
@@ -0,0 +1,5 @@
#!/usr/bin/env bash
ENVIRONMENT=${1:-production}
govc vm.ip -wait 5s turtle-js-"${ENVIRONMENT}"-*
+1
View File
@@ -0,0 +1 @@
s3://exp-artifacts/android-shell-builder-67bd02667e29221707f8c1dd11e739eada25ef55.tar.gz
+1
View File
@@ -0,0 +1 @@
s3://exp-artifacts/android-shell-builder-67bd02667e29221707f8c1dd11e739eada25ef55.tar.gz
+1
View File
@@ -0,0 +1 @@
s3://exp-artifacts/android-shell-builder-67bd02667e29221707f8c1dd11e739eada25ef55.tar.gz
+1
View File
@@ -0,0 +1 @@
s3://exp-artifacts/android-shell-builder-67bd02667e29221707f8c1dd11e739eada25ef55.tar.gz
+1
View File
@@ -0,0 +1 @@
s3://exp-artifacts/android-shell-builder-67bd02667e29221707f8c1dd11e739eada25ef55.tar.gz
+1
View File
@@ -0,0 +1 @@
s3://exp-artifacts/android-shell-builder-5c339ff7bab6f2161d378cc70e1bf04d32ad98dd.tar.gz
+1
View File
@@ -0,0 +1 @@
s3://exp-artifacts/android-shell-builder-cb03fdc12dc74f699a6d3f22219cbcd30e01ac46.tar.gz
+1
View File
@@ -0,0 +1 @@
s3://exp-artifacts/ios-shell-builder-sdk-26-db356a09877642d64023af98e4e2e1e632fbe4df.tar.gz
+1
View File
@@ -0,0 +1 @@
s3://exp-artifacts/ios-shell-builder-sdk-27-db356a09877642d64023af98e4e2e1e632fbe4df.tar.gz
+1
View File
@@ -0,0 +1 @@
s3://exp-artifacts/ios-shell-builder-sdk-28-db356a09877642d64023af98e4e2e1e632fbe4df.tar.gz
+1
View File
@@ -0,0 +1 @@
s3://exp-artifacts/ios-shell-builder-sdk-29-db356a09877642d64023af98e4e2e1e632fbe4df.tar.gz
+1
View File
@@ -0,0 +1 @@
s3://exp-artifacts/ios-shell-builder-sdk-30-7d017a96d00fbf18ee7e5581ea47528b9cc78afd.tar.gz
+1
View File
@@ -0,0 +1 @@
s3://exp-artifacts/ios-shell-builder-sdk-latest-56824ecea6ba10f5a9d8991545a1c47ee3284f5f.tar.gz
+1
View File
@@ -0,0 +1 @@
s3://exp-artifacts/ios-shell-builder-sdk-latest-e3855783ed0618f4dcf326dc353c6e2951e05c49.tar.gz
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,2 @@
recording.json filter=git-crypt diff=git-crypt
.gitatrributes !filter !diff
Binary file not shown.
+106
View File
@@ -0,0 +1,106 @@
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);
});
+1
View File
@@ -0,0 +1 @@
declare module 'nock';
+90
View File
@@ -0,0 +1,90 @@
{
"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":"/u3+7QAAAAIAAAABAAAAAQAUcWdma2J3bHVsM3JsYzNyYmNoYT0AAAFmV4eyTAAABQIwggT+MA4GCisGAQQBKgIRAQEFAASCBOqh06OSIFd3cqOpCLjFPGWWK1XcT6BRdDq1l1dFl8mPv6YAVKUQSZcKp69iM8AHVbPSLOfD+mgpbgpyZFnKSqxu6Qsn8N3wfw5IIW7hqmXE1kfhWj6T6n27mW6rWH02b0TZkPOUZB9A7kcYXbbF20X+pPJPdesMEaqaUT9qbkjPCvJmsNaXR71maQ8UsSpPnkrPVQeAs/wIr5tzNI/zRlnn57FFnvL8xZ9jrXFt4/2F0AdVEGeO0tZD78qTdpcoxp6hYVQ+T4qkjZgFGi5OWERa61BDqZeFOocohdC9GSGn1/WGjxQmKhWWAzDbrGbmqHOXyJofRH5nO/PwE4VFyIbCf4LWDALIrwdQ+xGE7rwwKAdVCD1Rw4K9o+tc8IBC6Z3+aTZ0wT4+Swsk9+1GPRPyWhkJwHjAjN3TeU7REplwgM4wAviK3Kyu7qjdK5cYnMUtwofYMkaV+ewCkSHUgc0YhUt0QWSmMe7eY1buVU4eEfW+RY9S5eyaGAXW0hy1EfKOktsE0DBLnCcdOzydS3dZQqmyFbh3DK0u6dFxra8JTlxuRgZOPYBGL1deMzvTBxCUSRIB6Kfd4vP3UYynqhmAFCF4W90Gae8S5pon6fBAckC9weBF534OoUSfBvKEBtAywVTayyLgXyEF2a2xb6RWB5LihigWEMtIajUwGOXOgcAGvd5VFT1JQLBUhvEz8uR+Ru1oj8gaEEXrhUTiRAMP7tcnRe+3SQaseMlpNMK91jzRyvYmEvy2bP3jyCkfSUiaWo7IYGQPJ5AIcqE2i3Zdqx3VskkaVlb03ObM2b7JQ9J/aOlwYlCFCqzLpyTbXgescDCIYZU5kSLekfBG6sNo0J/+Gb6yjNF0cA46aGdOhx0gKwIdLrAQtyOqryO+9TLBESG0CfVR8JoSUz8krMlx3FC7ODmwiTnaKnDvFxpuqJw8gQdnRcR6p+5YPolvGrF77ZvTNW2CyBrHCxs3J7pVwounezRHEF8E3gIVvWBbFTQcUpNHr8Bvw+A6KlXGknP4rfOS355FyHyvGWmyFF1pqOsIDeCFZPUfjhr244HU/yowNMgZUMnyORtxxypcGyBycrEVrCNDpHJHsyVm6ds5p5cZbrwH9oAcBUthmPh0VQ+us/4sirw8XHVJLORdO3AsckNdifN0bOdgsaSyDi/qaTyRKkhxLFenoqmY/WT6B5oiYizARmtUw3I0zqMU78wbfnde1294O4LD25b6drDwJt1u4eTfkXVts43+l1cnSNDc3yfzaWY6W4yxMe4lMiwgs+QyzfgYJsbip0CzvwvWMtPqCSkogpkJzpuLeFTm+e11g7iU2gWEWIM8x13iMxAV8/p3eVZAhyFpTjYc8QLzh3hy1MLWPMm46FKqkiT14IYFZdCGiFMoHaVuTTzMsF5a2kVN6tUEiWekWO3jDPpqDAWzSaOOkgvq804pBGhos2+aFlDe8yzUgkZ7iMoUTzTxzl9Kpj8IdIPOCHh69qxoDaYIH7qD8aa+w2fr1DoYJdEB0ymf7V1Xo8OAO0rtwG1vwbeFMpaRDTtC6GFcBAf8/ssvVW1Fw74Vzp0HwPP8qX9389n8i7sYjNbNW9hG8kwNtageuWC52bbgcGIsE1gVQE1TIPNoM55hp6F9UDKtGZZLlOiZmvLaIVlagAwEDPQB9hSrD3cIu3dRAAAAAQAFWC41MDkAAANhMIIDXTCCAkWgAwIBAgIEU+/IPjANBgkqhkiG9w0BAQsFADBfMQswCQYDVQQGEwJVUzEJMAcGA1UECBMAMQkwBwYDVQQHEwAxCTAHBgNVBAoTADEJMAcGA1UECxMAMSQwIgYDVQQDExtjb20ueW91cmNvbXBhbnkueW91cmFwcG5hbWUwHhcNMTgxMDA5MDYzMzIyWhcNNDYwMjI0MDYzMzIyWjBfMQswCQYDVQQGEwJVUzEJMAcGA1UECBMAMQkwBwYDVQQHEwAxCTAHBgNVBAoTADEJMAcGA1UECxMAMSQwIgYDVQQDExtjb20ueW91cmNvbXBhbnkueW91cmFwcG5hbWUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCrhk7Es7wymUpBi4B6o5oTT+mdo7dRovjbaO3S7oCRWlHYRVzEU9/BXgiEE4pxxl+vajyCV4on/28zs6lHoyKQNINiKK5KYJuozmmFoOIE1ovpDyy7PAyzxpaxyzLzZOpP/yNEbzNQQwm7qovkcQ69YLb+xoJDYOqK45kJPH9WMlb5OBfsmDn7E31rBm9JcOeW2Fv9lcos8rfXnh1rJBaQUn+z5JUmT0E5DHs1ExP6xvlZBrrMUUPXZSrmawbeZW6dGH6Ww39MwAMh0ZMsM2LaRKKnYQf/8MKWJgipJ3qTPubDnLnEpEagsvVBfP2GHZrktaYFold6kH7Kt5lzCTK9AgMBAAGjITAfMB0GA1UdDgQWBBRfRAQcMFpEFfESHenD4Cf/nYR/9zANBgkqhkiG9w0BAQsFAAOCAQEAHbi40Ks403+LpG+1U2ZdzDzWpZmR/DmIj/3eGSHoosFY5x1iTOctzMfx5kFhOy3bm4ZNrBcjvTFYcXYM7sfE8k4KvSwVx1nOcE4MmPImGBRC+8A9uhJTqqsVYuC9DpzfrC5YgSBSjUE7YKsKwoUGOZzvToRFvEhXoylqpXyYXir8066FuUlZAD2R19kF0k1jR4Uh2ZmRUjRrqffoIPO32IARIGUXB8+cMLxxzz1qVBQtzhGgIpPspTONZO0uqJYCRkbaW+WPFmn9T34IE+p56H/mfE54eoqn+u2Oya2tlWS+kWSA3af3/pifTihzY5YmbX4QTbpPyOSDTy7Vhsy5/wWnJk0jpRgSOIVlF0PIIAN65Get",
"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
}
+121
View File
@@ -0,0 +1,121 @@
{
"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": "MIIMDAIBAzCCC9YGCSqGSIb3DQEHAaCCC8cEggvDMIILvzCCBl8GCSqGSIb3\nDQEHBqCCBlAwggZMAgEAMIIGRQYJKoZIhvcNAQcBMBwGCiqGSIb3DQEMAQYw\nDgQIHhwZcA5nbm8CAggAgIIGGI+DEfPuHZZJLmXfz4zypgiqk5rlMLLjLXhJ\n0VY2pDp+QB1UOBCPVdCFTZqTrdsmWETRH4dBDrsq5blmK525KuW9MhvVT/UD\ntQVZBeVeH38w2kitTU3meU6vlmjm4UJJcSqbTyjl/FOoprqIVuMyFkuNbh9w\nB65UO97E+aLZdPsWdSN9LZ1Rshc1UmoqBA8jzOXmG7naMfvIZFiomldU7zfM\n/Lp6xuRXIJConP3q6+IAWulg3WtVbGnDkt8/zHkaBnR0TdZNz2/kWezEp/o+\n3b/TdGOmvK8duTYwvbSVQdp5Hx3SexGJk1SS80XF2yG0/+lNsLiAXaZA/uAa\nAw+QwHA4vnckhXP8EO3gSjQRDxtraI8G9sF30AAkLHHFzwEbh+G1pKuYw/SK\nyZKab4gRJDKX4UWgIs7dU8pyAD3wRaECV9pXB66j8aZeAxigvFz9dWz5dEsb\nNyvNZWGURC6LMpZkRWsqPUHUyHIYEooRokhhBxILdvkNSUXpdTWD8dBJ2aej\nUi3iiejrdUho2lZTWkBMAyA2TX43GNoSlzgXctcJCiP5Ytljzh3zRynwlGeG\n2paKrJWvYVRuktg8Ka2lzRWxuJUv8xIJmbOJj4yL8SviDlwTS1LzWT4PPLP+\nId/MZAacXBYCZ0zRDvzwnIXKXMTCt23jfBBECngfTpARxY4u2z6H1k24VL18\ngqJFqmGqPMV7uGpQcmMrALkvBhY5swIvcY2VSmTaMTd8Q3XpNh7mpvmiAEpt\nzZw+fsDsfM+nR4B2Rt8fTK8UhnRBpVURnBhLI+4BVeVtSuCW1x8Y18KEuVCh\nsAqymDZ1otV2OHUCjmqZZ1VMmd/vHwhG7DfFhn9MOrjqdlcOQeOJCEVTRNq2\nDMFxaoTsIZUHC3CmNke92ZBp84MHxGiN+A3QlENf65GIlLYS20qTSIwbIU4r\nerLnrqEhO0t6MkCdN1e4Z1EaOY/4jksDsSNNB+CNS+Zwaz5lsJvWfSaqHsEM\nmL6jMtoR5Dr7EP3mSRrHe7QD9IiEa6mVytYTIQeqyAELrx5dhPg1is16KkM+\nsuoC0PAe0FzJlMluESk+m0dUixvc0WQYFhVQqM0Zpaik5XrO1tPMQeX6gejK\ndUUxDKM7SoJ0BWOZcvLaQon7Vx9fH24s/htVdlhOTKNexpc42sDHVQhUc1Tk\nf0td4ZY9Xh/gATX1oZXajcsiCyZidzZ0RxqJnSdqk6pVsH0dpo/5ea5DlLLJ\nR05wJ6VvrDEn9ZpsrHoMYstqcf56ggwlYd8uuxiQY2ksu6FB8e7Kg5orJOxs\nG9juMHXSZpW3WkIM+sMIfTC5JnNCEdqyu+jiUSL911ZMogAfuvpHgQz4lpPy\n784Qp2yMLBXMdxxOD0LTW/VOv4caYVLJrCx35Bm2P1z39x/iqQFcBuRb4OrX\nxKuBROzUXvrUrdA5lvH1JEf2eeR9Q/1ubMZLSOIFNvTMWyThSjTsp2MaS8vZ\nh2vehXIoxAurDChhbjTdO4lZ7WilcFdHT4zUFdXZ/Qv06POHX6UoyCj8Tvq8\nge5550oVK+TAanFHkqUEUHVIEXnV2VL6opFykGayWnghRIHE0I+HYIPcNj6T\nFQJdKAS0XPEU3HBNWeGwpCwmqorEVfhBW4QFf9lsq/O4++IuEICyEiqPtSnq\nf9Crbn0h0Mqe/2MeQYwKHg8RmDOYtXztvZp47SzAid1prUNYp2Kij+CjMaFl\nl0f1CjIQM/50Ron63O5BKGdq7yY9GOsoqqEXcoIgKkqKyIoZzO4aVP+V/z7s\nHO5aXoftFSSaYCGQDb/ryDOTnVGo5wAQiEpw+Ev1xFCpWxv2PLJFoOujNfam\nKhMVyd6f5lfkPn73kK+fx1bCT9r35Q7FUznVJfF6Qn6RI7i2jGlhtEgj4B2y\nsuN8dhgUdfvjnRgHxlHjiXvjC5Ky/Gb8YAyRjHchLLyrpcHQsw313VK6DG/9\nJE1P8pyWv64rVgF9o0kgf/ZkblJhyWhMM2wv97kIZyYFfNzcBrcWaiwZtSyZ\nWHTjhlXRzOMWjB1cXwlQEn7fGYhfRn07kH2WNj8bHKtF8IeB+0jTLAWlwmFf\nIfEa7jCCBVgGCSqGSIb3DQEHAaCCBUkEggVFMIIFQTCCBT0GCyqGSIb3DQEM\nCgECoIIE7jCCBOowHAYKKoZIhvcNAQwBAzAOBAj8gLGqnw8AuQICCAAEggTI\nyUPNA72xnCo7DZGqZ1RxypDFv6kzvX1iVdWGjVaKt3ppMYKIYazzlyJhJ9+n\n5apYRMKh0KBFCoDf5RuZ3pyPv8EDj8ed+KUw/8YeRBRYLoXHPQnIIUsuj+Rv\nHsMt4dulCWBqOee03zDi0cDG9vDpzSpXdNkpoaTpb1AKGIXJgOj3W5bguFFq\nj+C07DJAYf1qTxxzhMVolLBY9FBb91G1IR8n3m6HcgGM4MkAkK1UDnfUAICf\nMMUB/VSqUfM2NvWUnQDbIg0rE5XeJx4a9llBvC9rAu/kil94QjetmBWIdv4/\n22EqNBwfDiaUCPvb0YN5vvyeS1TE6TfefMysbTjbrbsRUm82NldMLYixbnvx\nUw8GObbbkw1NUXihhnTrCQL+KgC7QBsVKP4s4PwARZWPNYyIL0WlORiU8N65\nYwKlWBZXRkweXMyA7lTs9f+RkJw37jMvspHMHRDTPKnXfC8mHf//2roBTkv8\neogzHtDwZeB1O10MR1+2tXxzo2zoY6+M4421jnG8GfUZMCXn/5fnEbgpxV7w\nRIFVJScuC+nA0CxMhNmJn3pYij8VYKMas0y/kTFa77l2qn6a+Od4c+WPtwOF\nFfHXb7OMHYFkeOVjlZ6Ph2HJMZpkpzsXC5SnfYwnqV30soa6gcOsnf6xSKSs\nKlrDSJsP2brd5cDIXKsuB4UdKHGIU4qZabKMHo2MWeDiU/HZ02oGQbT1ABjO\niJuNYu7lvW22eDTFkG6kxYZ/ULt7d6AFRHYETkxApIwWgskF9WxLCYtvgo44\nfu8e9DH7hIVWcWcCRvpB3MS1gnquITKQ3eaITQ/eTELucf8S/FC9t+VpSqS5\nLhcFKyMUHvFc6tZ/JmZjYqxgbqjGNDWn3SuycTnbH0Nt+hMlN/EiZHrlo/TO\nrXHLwq2Rpe/kMMGFjCZH4GT+ewiZ9rU5ct2mpeOHNkVnuQ8iB3De0dVuaFwB\n3nJURL14BbhdI4mxnpUq5+YK4yaNzeIJQBKYVu8m3nsVwcolDeTlSMlh/D8l\nRgKFG2GCOGhhmnomm+GqZTvwGliYJaKYfuk7O6CorV5r9pTG9ctJHqU6JkUw\nqKO4S6pSEs0eGyApbktDQp58jHrsQ8lW2MSBYDbj9qC0NQ8cOyrnR3skFkxv\nq2IzPF2tA2eU9UQ4GvxFZF+3s2U0ZVDR2aemZ7SCB9Zfq9XDmpD3QeLiU9f3\n46r31bAUtXAL/xqkfEJRqGWqF6koi/FmrpHIB2T3ZCTMPdpb5OfZnC1hzU9m\nhNjVKjhpVljEzZQanPRlvNWqTpP6eqKc0Uy/cGDEoQDjggKO5UHMrpxPcjcV\nR/i395psBujpABLeokhRdNEwCo2oh0vjiQCEYiTAN8Dm/CvYUzc+T8mpkvSe\nYqn279KUij8d3ni3nh2gbcUH2dGMeyv6MI7n2kW1h1y2Py15kMAP44D1CUz5\n14ktseww2TL/iHr3SFCTEjGs96cqjWy+DYx6DCc8eNy6CH5+BE0DeAVOhSqf\nijLlaocv5D45XySOabGsu8mwU6eUOIewlvRUTlGLHOr8E5K8i78yKMPOBs3a\nIJgg2l4CP8UAccu9XTUFVCM3kZ44jqzkrwlBmShTjPtyYLcFCGN+RVH+WmSZ\nC/s2fuilN4hIMTwwFQYJKoZIhvcNAQkUMQgeBgBrAGUAeTAjBgkqhkiG9w0B\nCRUxFgQULerKBMEcCXTToElFKvVD4NyZgkIwLTAhMAkGBSsOAwIaBQAEFFuQ\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
}
+5
View File
@@ -0,0 +1,5 @@
import * as context from 'turtle/turtleContext';
test('Initially should not exit', () => {
expect(context.checkShouldExit()).toBe(false);
});
+14
View File
@@ -0,0 +1,14 @@
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');
});
+2
View File
@@ -0,0 +1,2 @@
export const EXTRA_DIMENSIONS = Symbol('Extra Dimensions');
export const GROUP_HASH = Symbol('Group Hash');
+15
View File
@@ -0,0 +1,15 @@
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 },
],
];
+186
View File
@@ -0,0 +1,186 @@
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);
}
});
});
}
+27
View File
@@ -0,0 +1,27 @@
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];
}
+29
View File
@@ -0,0 +1,29 @@
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];
}
+48
View File
@@ -0,0 +1,48 @@
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[];
}
+28
View File
@@ -0,0 +1,28 @@
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();
}
+91
View File
@@ -0,0 +1,91 @@
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);
}
}
+4
View File
@@ -0,0 +1,4 @@
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;
+2
View File
@@ -0,0 +1,2 @@
export class ErrorWithCommandHelp extends Error {
}
+2
View File
@@ -0,0 +1,2 @@
export class ErrorWithProgramHelp extends Error {
}
+74
View File
@@ -0,0 +1,74 @@
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,
};
}
};
+29
View File
@@ -0,0 +1,29 @@
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),
};
+82
View File
@@ -0,0 +1,82 @@
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'),
};
};
+8
View File
@@ -0,0 +1,8 @@
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;
+50
View File
@@ -0,0 +1,50 @@
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),
};
+51
View File
@@ -0,0 +1,51 @@
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);
});
}
+70
View File
@@ -0,0 +1,70 @@
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`,
);
}
}
+114
View File
@@ -0,0 +1,114 @@
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(':');
}
+51
View File
@@ -0,0 +1,51 @@
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];
}
+78
View File
@@ -0,0 +1,78 @@
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'),
];
}
+49
View File
@@ -0,0 +1,49 @@
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}`);
}
+19
View File
@@ -0,0 +1,19 @@
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 :(');
}
}
+66
View File
@@ -0,0 +1,66 @@
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();
}
+47
View File
@@ -0,0 +1,47 @@
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}`;
}
+37
View File
@@ -0,0 +1,37 @@
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.');
}
}
+85
View File
@@ -0,0 +1,85 @@
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);
}
};
}
+22
View File
@@ -0,0 +1,22 @@
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;
}
};
+23
View File
@@ -0,0 +1,23 @@
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');
}
}
}
+91
View File
@@ -0,0 +1,91 @@
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;
}
+7
View File
@@ -0,0 +1,7 @@
import buildAndroid from './android';
import buildIOS from './ios';
export default {
ios: buildIOS,
android: buildAndroid,
};
+62
View File
@@ -0,0 +1,62 @@
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`);
}
+91
View File
@@ -0,0 +1,91 @@
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;
}
+84
View File
@@ -0,0 +1,84 @@
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));
}
+11
View File
@@ -0,0 +1,11 @@
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');
}
+55
View File
@@ -0,0 +1,55 @@
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;
+13
View File
@@ -0,0 +1,13 @@
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}`);
}
+26
View File
@@ -0,0 +1,26 @@
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;
}
}
+6
View File
@@ -0,0 +1,6 @@
import fs from 'fs-extra';
export async function writeBase64ToBinaryFile(filename: string, base64: string) {
const buffer = Buffer.from(base64, 'base64');
await fs.writeFile(filename, buffer);
}
+33
View File
@@ -0,0 +1,33 @@
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;
}
}
+63
View File
@@ -0,0 +1,63 @@
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;
}
}
+52
View File
@@ -0,0 +1,52 @@
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);
}
+13
View File
@@ -0,0 +1,13 @@
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}`);
}
+31
View File
@@ -0,0 +1,31 @@
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;
}
}
+91
View File
@@ -0,0 +1,91 @@
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'),
),
},
};
+12
View File
@@ -0,0 +1,12 @@
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