mirror of
https://github.com/expo/turtle.git
synced 2026-11-14 19:12:55 -10:00
Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0da20f4ab5 | ||
|
|
05d55bf731 | ||
|
|
074ba521f9 | ||
|
|
451999f0b9 |
No files matched your search
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"presets": [
|
||||
["env", {
|
||||
"targets": {
|
||||
"node": "8.9"
|
||||
}
|
||||
}]
|
||||
],
|
||||
"plugins": [
|
||||
["module-resolver", {
|
||||
"alias": {
|
||||
"turtle": "./build"
|
||||
}
|
||||
}]
|
||||
]
|
||||
}
|
||||
@@ -3,7 +3,7 @@ version: 2.1
|
||||
executors:
|
||||
nix: &nix
|
||||
docker:
|
||||
- image: lnl7/nix:2.2
|
||||
- image: lnl7/nix:2.1.2
|
||||
# Symlink to the readline-enabled bash which is the default command of the container
|
||||
shell: /run/current-system/sw/bin/bash -leo pipefail
|
||||
working_directory: ~/turtle
|
||||
@@ -33,12 +33,10 @@ commands:
|
||||
command: |
|
||||
nix-env -iA \
|
||||
nixpkgs.binutils \
|
||||
nixpkgs.curl \
|
||||
nixpkgs.direnv \
|
||||
nixpkgs.findutils \
|
||||
nixpkgs.gawk \
|
||||
nixpkgs.git \
|
||||
nixpkgs.gnused \
|
||||
nixpkgs.gnutar \
|
||||
nixpkgs.gzip \
|
||||
nixpkgs.openssh \
|
||||
@@ -139,12 +137,11 @@ commands:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
executor: nix
|
||||
docker:
|
||||
- image: circleci/node:8.12
|
||||
steps:
|
||||
- setup
|
||||
- restore_yarn_cache
|
||||
- checkout
|
||||
- run: yarn
|
||||
- save_yarn_cache
|
||||
- run: yarn lint
|
||||
- run: yarn build
|
||||
|
||||
@@ -156,7 +153,7 @@ jobs:
|
||||
- run: yarn
|
||||
- save_yarn_cache
|
||||
- run:
|
||||
name: Run unit tests
|
||||
name: Run iOS smoke tests
|
||||
command: ./scripts/tests/runUnitTestsCI.sh
|
||||
|
||||
ios_smoke_tests:
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/usr/bin/env bash
|
||||
#!/bin/bash
|
||||
|
||||
source_local() {
|
||||
file=./.envrc.local
|
||||
@@ -17,7 +17,4 @@ export EXPO_TURTLE_TOOLS_DIR="$(pwd)/tools"
|
||||
|
||||
if [ -z "$EXPO_UNIVERSE_DIR" ]; then
|
||||
PATH_add $EXPO_TURTLE_TOOLS_DIR/bin
|
||||
if [ "$CI" = "true" ]; then
|
||||
PATH_add $EXPO_TURTLE_TOOLS_DIR/ci/bin
|
||||
fi
|
||||
fi
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
node_modules
|
||||
yarn-error.log
|
||||
google-cloud-credentials.json
|
||||
|
||||
/build/
|
||||
/workingdir/*
|
||||
|
||||
+2
-60
@@ -6,69 +6,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
## [Unreleased]
|
||||
### Changed
|
||||
- Updated Android shell app for SDK33 to remove `expo-face-detector` and `expo-payments-stripe` from standalone builds.
|
||||
- Updated Android shell app for SDK33 to get rid of `node_modules` for `tools-public`.
|
||||
- From now on, `turtle-cli` installs dependencies for `tools-public` in shell apps for SDK versions < 33.
|
||||
### Fixed
|
||||
- Android builds with turtle-cli by installing all unmodules if `dependencies` is empty.
|
||||
|
||||
## [0.8.1] - 2019-06-10
|
||||
### Changed
|
||||
- Add expo dependencies when resolving unimodules.
|
||||
### Fixed
|
||||
- Support for Android tarballs without `universe-package.json`.
|
||||
|
||||
## [0.8.0] - 2019-06-07
|
||||
### Added
|
||||
- Turtle CLI logo to the project (created by [@zularizal](https://github.com/zularizal))!
|
||||
- Shell apps for SDK 33.
|
||||
- Support for Android App Bundles.
|
||||
### Changed
|
||||
- Updated Android shell app for SDK 32 to fix AppAuth issue (https://github.com/expo/expo/pull/4115) and to fix a newer than expected JSC version.
|
||||
- Default value for build timeout to 15 minutes.
|
||||
- Added filtering of logs (strips out download progress) when downloading SDK tools.
|
||||
- `xdl` -> `@expo/xdl`, updated `@expo/xdl`.
|
||||
### Fixed
|
||||
- Generating Android keystore in turtle when one is not passed to turtle-cli (https://github.com/expo/turtle/pull/65).
|
||||
- Upload logs to s3 if build has been canceled or timed out.
|
||||
- Unhandled exception on asset download (xdl).
|
||||
|
||||
## [0.7.2] - 2019-05-09
|
||||
### Added
|
||||
- Support for Android tarballs without `universe-package.json`.
|
||||
### Changed
|
||||
- Upgraded `@expo/traveling-fastlane-darwin` to 1.9.4 (fixes issues with generating provisioning profiles for ad-hoc builds).
|
||||
### Removed
|
||||
- Support for `CLIENT_LOGGER_LEVEL` env variable.
|
||||
|
||||
## [0.7.1] - 2019-05-06
|
||||
### Fixed
|
||||
- Resolving `fastlane` version when `LC_ALL` is not set.
|
||||
|
||||
## [0.7.0] - 2019-05-06
|
||||
### Added
|
||||
- Support for custom Android builds.
|
||||
### Changed
|
||||
- Refactored logger.
|
||||
### Fixed
|
||||
- `platform` log field wrongly set to `ios` for Android builds (when using `turtle-cli`).
|
||||
- Brought back `jobID` and `experienceName` fields to Android logs.
|
||||
|
||||
## [0.6.1] - 2019-04-23
|
||||
### Added
|
||||
- Checking fastlane version before running build (turtle-cli).
|
||||
### Changed
|
||||
- Updated iOS shell app for SDK 32 to fix AppAuth issue.
|
||||
- Babel replaced by ttypescript.
|
||||
- Updated xdl to 53.5.3 and @expo/spawn-async to 1.4.2, so that error stack traces should be more informative now.
|
||||
### Fixed
|
||||
- Brought back `jobID` and `experienceName` fields to GCloud logs.
|
||||
### Removed
|
||||
- `buidAndroidTarballLocally.sh` script (it has been moved to expo repository - [expo/expo#4022](https://github.com/expo/expo/pull/4022)).
|
||||
- Updated iOS shell app for SDK 32 to fix AppAuth issue
|
||||
|
||||
## [0.6.0] - 2019-04-09
|
||||
### Changed
|
||||
- Removed Branch module from Android shell apps for all SDK versions.
|
||||
- Removed Branch module from Android shell apps for all SDK versions
|
||||
|
||||
## [0.5.15] - 2019-04-03
|
||||
### Added
|
||||
|
||||
+14
-19
@@ -20,7 +20,6 @@ RUN dpkg --add-architecture i386 && \
|
||||
libc6:i386\
|
||||
libncurses5:i386\
|
||||
libstdc++6:i386\
|
||||
rsync\
|
||||
unzip\
|
||||
yarn\
|
||||
zlib1g:i386\
|
||||
@@ -58,13 +57,13 @@ RUN yes | sdkmanager \
|
||||
"platforms;android-25" \
|
||||
"platforms;android-26" \
|
||||
"platforms;android-27" \
|
||||
"platforms;android-28" | grep -v '=' || echo 'no output'
|
||||
RUN yes | sdkmanager "platform-tools" | grep -v '=' || echo 'no output'
|
||||
RUN yes | sdkmanager "build-tools;28.0.3" | grep -v '=' || echo 'no output'
|
||||
"platforms;android-28"
|
||||
RUN yes | sdkmanager "platform-tools"
|
||||
RUN yes | sdkmanager "build-tools;28.0.3"
|
||||
RUN yes | sdkmanager \
|
||||
"extras;android;m2repository" \
|
||||
"extras;google;m2repository" \
|
||||
"extras;google;google_play_services" | grep -v '=' || echo 'no output'
|
||||
"extras;google;google_play_services"
|
||||
|
||||
# Install Android NDK
|
||||
ENV ANDROID_NDK_VERSION android-ndk-r10e
|
||||
@@ -100,27 +99,23 @@ RUN rm gradle-4.4-all.zip
|
||||
ENV GRADLE_HOME /usr/local/gradle-4.4
|
||||
ENV PATH ${GRADLE_HOME}/bin:$PATH
|
||||
|
||||
ADD . /app/turtle
|
||||
# Install Gulp
|
||||
RUN npm install -g gulp-cli
|
||||
|
||||
RUN mv /app/turtle/workingdir /app && \
|
||||
for SDK_VERSION in `ls /app/workingdir/android/`; do \
|
||||
echo "preparing $SDK_VERSION shell app" && \
|
||||
ADD . /app
|
||||
|
||||
RUN for SDK_VERSION in `ls /app/workingdir/android/`; do \
|
||||
cd /app/workingdir/android/$SDK_VERSION && \
|
||||
if [ -f universe-package.json ]; then \
|
||||
mv package.json exponent-package.json && \
|
||||
mv universe-package.json package.json && \
|
||||
yarn install && \
|
||||
mv package.json universe-package.json && \
|
||||
mv exponent-package.json package.json; \
|
||||
else \
|
||||
yarn install; \
|
||||
fi \
|
||||
; done && \
|
||||
mv /app/workingdir /app/turtle
|
||||
mv exponent-package.json package.json \
|
||||
; done
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
ENV NODE_ENV "production"
|
||||
ENV TURTLE_WORKING_DIR_PATH /app/turtle/workingdir/
|
||||
|
||||
WORKDIR /app/turtle
|
||||
ENV TURTLE_WORKING_DIR_PATH /app/workingdir/
|
||||
|
||||
CMD ["node", "./build/server.js"]
|
||||
@@ -1,6 +1,4 @@
|
||||
<h3 align=center>
|
||||
<img src="logo/horizontal.png" width=50%>
|
||||
</h3>
|
||||
# Turtle CLI
|
||||
|
||||
[](https://circleci.com/gh/expo/turtle)
|
||||
[](https://npmjs.com/package/turtle-cli)
|
||||
@@ -9,8 +7,6 @@
|
||||
[](http://slack.expo.io)
|
||||
[](https://forums.expo.io/)
|
||||
|
||||
<hr />
|
||||
|
||||
Turtle CLI is a command line interface for building Expo standalone apps.
|
||||
You can use it both on your CI and your private computer.
|
||||
|
||||
|
||||
@@ -1,15 +1,6 @@
|
||||
#!/usr/bin/env node
|
||||
'use strict';
|
||||
|
||||
const subcommand = process.argv[2];
|
||||
if (subcommand) {
|
||||
const [, platform] = subcommand.split(':');
|
||||
if (platform === 'ios' || platform === 'android') {
|
||||
// so that logs have correct platform set
|
||||
process.env.PLATFORM = platform;
|
||||
}
|
||||
}
|
||||
|
||||
require('source-map-support').install();
|
||||
require('../build/bin/env').initOfflineEnv();
|
||||
require('../build/bin/index').run('turtle');
|
||||
+91
@@ -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'));
|
||||
@@ -22,7 +22,7 @@ spec:
|
||||
environment: ${NAMESPACE}
|
||||
spec:
|
||||
restartPolicy: Always
|
||||
terminationGracePeriodSeconds: 1200 ## wait 20 minutes for any builds to complete
|
||||
terminationGracePeriodSeconds: 1800 ## wait 30 minutes for any builds to complete
|
||||
tolerations:
|
||||
- key: dedicated
|
||||
operator: Equal
|
||||
@@ -128,16 +128,6 @@ spec:
|
||||
secretKeyRef:
|
||||
name: turtle-js-config
|
||||
key: api_port
|
||||
- name: DATADOG_API_KEY
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: turtle-js-config
|
||||
key: datadog_api_key
|
||||
- name: DATADOG_APP_KEY
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: turtle-js-config
|
||||
key: datadog_app_key
|
||||
# other vars
|
||||
- name: GRADLE_DAEMON_DISABLED
|
||||
value: "1"
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 69 KiB |
@@ -1,228 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
width="151.68256mm"
|
||||
height="150.14914mm"
|
||||
viewBox="0 0 151.68255 150.14914"
|
||||
version="1.1"
|
||||
id="svg8"
|
||||
sodipodi:docname="logo-only.svg"
|
||||
inkscape:version="0.92.4 (5da689c313, 2019-01-14)">
|
||||
<defs
|
||||
id="defs2">
|
||||
<clipPath
|
||||
clipPathUnits="userSpaceOnUse"
|
||||
id="clipPath16">
|
||||
<path
|
||||
d="M 0,192 H 192 V 0 H 0 Z"
|
||||
id="path18"
|
||||
inkscape:connector-curvature="0" />
|
||||
</clipPath>
|
||||
<clipPath
|
||||
clipPathUnits="userSpaceOnUse"
|
||||
id="clipPath26">
|
||||
<path
|
||||
d="M -4.88281e-4,192 H 192 V -4.88281e-4 H -4.88281e-4 Z"
|
||||
id="path28"
|
||||
inkscape:connector-curvature="0" />
|
||||
</clipPath>
|
||||
<clipPath
|
||||
clipPathUnits="userSpaceOnUse"
|
||||
id="clipPath34">
|
||||
<path
|
||||
d="M -4.88281e-4,192 H 192 V -4.88281e-4 H -4.88281e-4 Z"
|
||||
id="path36"
|
||||
inkscape:connector-curvature="0" />
|
||||
</clipPath>
|
||||
<clipPath
|
||||
clipPathUnits="userSpaceOnUse"
|
||||
id="clipPath38">
|
||||
<path
|
||||
d="M 0,192 H 192 V 0 H 0 Z"
|
||||
id="path40"
|
||||
inkscape:connector-curvature="0" />
|
||||
</clipPath>
|
||||
<clipPath
|
||||
clipPathUnits="userSpaceOnUse"
|
||||
id="clipPath42">
|
||||
<path
|
||||
d="M 0,192 H 192 V 0 H 0 Z"
|
||||
id="path44"
|
||||
inkscape:connector-curvature="0" />
|
||||
</clipPath>
|
||||
<clipPath
|
||||
clipPathUnits="userSpaceOnUse"
|
||||
id="clipPath116">
|
||||
<path
|
||||
d="M 0,192 H 192 V -4.88281e-4 H 0 Z"
|
||||
id="path118"
|
||||
inkscape:connector-curvature="0" />
|
||||
</clipPath>
|
||||
<clipPath
|
||||
clipPathUnits="userSpaceOnUse"
|
||||
id="clipPath120">
|
||||
<path
|
||||
d="M 0,192 H 192 V 4.88281e-4 H 0 Z"
|
||||
id="path122"
|
||||
inkscape:connector-curvature="0" />
|
||||
</clipPath>
|
||||
<clipPath
|
||||
clipPathUnits="userSpaceOnUse"
|
||||
id="clipPath124">
|
||||
<path
|
||||
d="M 192,192 H 0 V 0 h 192 z"
|
||||
id="path126"
|
||||
inkscape:connector-curvature="0" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
<sodipodi:namedview
|
||||
id="base"
|
||||
pagecolor="#3d3d3d"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:zoom="0.49497474"
|
||||
inkscape:cx="-192.6803"
|
||||
inkscape:cy="237.87441"
|
||||
inkscape:document-units="px"
|
||||
inkscape:current-layer="layer2"
|
||||
showgrid="false"
|
||||
units="mm"
|
||||
inkscape:showpageshadow="false"
|
||||
inkscape:window-width="1366"
|
||||
inkscape:window-height="705"
|
||||
inkscape:window-x="-8"
|
||||
inkscape:window-y="-8"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:object-paths="true"
|
||||
fit-margin-top="0"
|
||||
fit-margin-left="0"
|
||||
fit-margin-right="0"
|
||||
fit-margin-bottom="0" />
|
||||
<metadata
|
||||
id="metadata5">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title></dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<g
|
||||
inkscape:groupmode="layer"
|
||||
id="layer2"
|
||||
inkscape:label="Layer 2"
|
||||
transform="translate(-29.1587,91.282943)">
|
||||
<g
|
||||
id="g6678"
|
||||
transform="translate(792.65276,-237.50395)">
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
id="path6638"
|
||||
d="m -687.31547,152.53653 -16.98554,9.86968 v 19.204 l 15.52774,-8.98033 a 2.9187589,2.9239263 0 0 1 1.52654,-0.38809 2.9187589,2.9239263 0 0 1 1.39009,0.38809 l 15.52721,8.98033 v -19.204 z"
|
||||
style="opacity:1;fill:#00002f;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1.32291651;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:stroke fill markers" />
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
id="path6640"
|
||||
d="m -687.3165,178.53959 -48.20947,27.88306 v 55.7661 l 48.20947,27.88306 48.20996,-27.88306 v -55.7661 z"
|
||||
style="opacity:1;fill:#00002f;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1.32291651;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:stroke fill markers" />
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
id="path6642"
|
||||
d="m -738.44361,184.99346 -16.985,9.86968 v 19.74092 l 14.06631,8.17418 v -18.04386 a 2.9187589,2.9239263 0 0 1 1.45986,-2.5337 l 15.57269,-9.00617 z"
|
||||
style="opacity:1;fill:#00002f;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1.32291651;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:stroke fill markers" />
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
id="path6644"
|
||||
d="m -634.61997,184.99346 -14.89522,8.65426 14.78772,8.55296 a 2.9187589,2.9239263 0 0 1 1.45986,2.5337 v 18.95491 l 15.63418,-9.08523 v -19.74092 z"
|
||||
style="opacity:1;fill:#00002f;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1.32291651;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:stroke fill markers" />
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
id="path6646"
|
||||
d="m -741.3623,245.97166 -16.30752,9.47642 v 19.74091 l 16.98398,9.87124 16.47753,-9.57513 -15.69413,-9.07748 a 2.9187589,2.9239263 0 0 1 -1.45986,-2.5306 z"
|
||||
style="opacity:1;fill:#00002f;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1.32291651;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:stroke fill markers" />
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
id="path6648"
|
||||
d="m -633.26761,247.66664 v 16.21038 a 2.9187589,2.9239263 0 0 1 -1.45986,2.5306 l -17.35915,10.0397 v 5.2e-4 l 0.20309,-0.11782 15.02182,8.73022 16.98655,-9.87124 v -19.74091 z"
|
||||
style="opacity:1;fill:#00002f;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1.32291651;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:stroke fill markers" />
|
||||
<g
|
||||
id="g6654"
|
||||
transform="translate(-172.35714,278.19048)">
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
id="path6650"
|
||||
d="m -514.88984,-105.94886 a 2.918759,2.9239263 0 0 0 -1.52625,0.388 l -51.12915,29.57121 a 2.918759,2.9239263 0 0 0 -1.45977,2.5338 v 59.142302 a 2.918759,2.9239263 0 0 0 1.45977,2.530901 l 51.12915,29.571197 a 2.918759,2.9239263 0 0 0 2.91661,0 l 51.12917,-29.571197 a 2.918759,2.9239263 0 0 0 1.45977,-2.530901 V -73.45585 a 2.918759,2.9239263 0 0 0 -1.45977,-2.5338 l -51.12917,-29.57121 a 2.918759,2.9239263 0 0 0 -1.39036,-0.388 z m -0.0694,6.298204 48.20963,27.882906 v 55.766001 l -48.20963,27.883001 -48.20965,-27.883001 V -71.76775 Z"
|
||||
style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-feature-settings:normal;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;text-orientation:mixed;dominant-baseline:auto;baseline-shift:baseline;text-anchor:start;white-space:normal;shape-padding:0;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;vector-effect:none;fill:#21aba5;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:3.17500019;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:stroke fill markers;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" />
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
id="path6652"
|
||||
d="m -514.88885,-85.717946 a 2.9118515,2.9308623 0 0 0 -1.52531,0.3904 l -33.47339,19.451796 a 2.9118515,2.9308623 0 0 0 -1.45528,2.5391 v 38.903402 a 2.9118515,2.9308623 0 0 0 1.45528,2.537198 l 33.47339,19.4517023 a 2.9118515,2.9308623 0 0 0 2.91246,0 l 33.47151,-19.4517023 a 2.9118515,2.9308623 0 0 0 1.45528,-2.537198 V -63.33665 a 2.9118515,2.9308623 0 0 0 -1.45528,-2.5391 l -33.47151,-19.451796 a 2.9118515,2.9308623 0 0 0 -1.38715,-0.3904 z m -0.0699,6.312396 30.56093,17.760299 V -26.12465 L -514.95875,-8.3663467 -545.5197,-26.12665 v -35.518601 z"
|
||||
style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-feature-settings:normal;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;text-orientation:mixed;dominant-baseline:auto;baseline-shift:baseline;text-anchor:start;white-space:normal;shape-padding:0;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;vector-effect:none;fill:#21aba5;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:3.17500019;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:stroke fill markers;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" />
|
||||
</g>
|
||||
<g
|
||||
id="g6660"
|
||||
transform="translate(-172.35714,278.19048)">
|
||||
<path
|
||||
id="path6656"
|
||||
d="m -519.66912,-32.288849 -5.84307,5.844 h 25.07044 a 2.9220001,2.9220001 0 0 0 0,-5.844 z"
|
||||
style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-feature-settings:normal;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;text-orientation:mixed;dominant-baseline:auto;baseline-shift:baseline;text-anchor:start;white-space:normal;shape-padding:0;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;vector-effect:none;fill:#45eba5;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:3.17499971;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
|
||||
inkscape:connector-curvature="0" />
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
id="path6658"
|
||||
d="m -529.50808,-61.355249 a 2.9213416,2.9213416 0 0 0 -2.0341,5.017 l 12.45256,12.452501 -12.45256,12.452599 a 2.9213416,2.9213416 0 1 0 4.1329,4.129201 l 14.51539,-14.515401 a 2.9213416,2.9213416 0 0 0 0,-4.1328 l -14.51539,-14.515401 a 2.9213416,2.9213416 0 0 0 -2.0988,-0.887699 z"
|
||||
style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-feature-settings:normal;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;text-orientation:mixed;dominant-baseline:auto;baseline-shift:baseline;text-anchor:start;white-space:normal;shape-padding:0;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;vector-effect:none;fill:#45eba5;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:3.17500019;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" />
|
||||
</g>
|
||||
<g
|
||||
id="g6676"
|
||||
transform="translate(-172.35714,278.19048)">
|
||||
<g
|
||||
id="g6672">
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-feature-settings:normal;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;text-orientation:mixed;dominant-baseline:auto;baseline-shift:baseline;text-anchor:start;white-space:normal;shape-padding:0;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;vector-effect:none;fill:#21aba5;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:3.17499971;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:stroke fill markers;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
|
||||
d="m -514.89008,-131.96866 a 2.9118515,2.9308623 0 0 0 -1.52426,0.3917 l -19.89585,11.5625 a 2.9118515,2.9308623 0 0 0 -1.45673,2.5388 v 23.125004 a 2.9118515,2.9308623 0 0 0 0.18924,1.02881 l 5.63388,-3.25861 v -19.203604 l 16.98527,-9.87 16.98619,9.87 v 19.203604 l 5.6348,3.25861 a 2.9118515,2.9308623 0 0 0 0.18734,-1.02881 v -23.125004 a 2.9118515,2.9308623 0 0 0 -1.4558,-2.5388 l -19.89678,-11.5625 a 2.9118515,2.9308623 0 0 0 -1.3873,-0.3917 z"
|
||||
id="path6662" />
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-feature-settings:normal;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;text-orientation:mixed;dominant-baseline:auto;baseline-shift:baseline;text-anchor:start;white-space:normal;shape-padding:0;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;vector-effect:none;fill:#21aba5;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:3.17499971;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:stroke fill markers;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
|
||||
d="m -566.01799,-99.511956 a 2.9118515,2.9308623 0 0 0 -1.52421,0.3918 l -19.89585,11.56251 a 2.9118515,2.9308623 0 0 0 -1.45673,2.5387 v 23.124998 a 2.9118515,2.9308623 0 0 0 1.45673,2.537899 l 18.43343,10.7124 v -6.768201 l -14.06704,-8.174598 v -19.740898 l 16.98522,-9.8699 14.11368,8.2012 5.83733,-3.3766 -18.49521,-10.74751 a 2.9118515,2.9308623 0 0 0 -1.38735,-0.3918 z"
|
||||
id="path6664" />
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-feature-settings:normal;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;text-orientation:mixed;dominant-baseline:auto;baseline-shift:baseline;text-anchor:start;white-space:normal;shape-padding:0;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;vector-effect:none;fill:#21aba5;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:3.17499971;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:stroke fill markers;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
|
||||
d="m -462.19425,-99.511956 a 2.9118515,2.9308623 0 0 0 -1.52421,0.3918 l -19.27591,11.20211 5.83641,3.3755 14.89526,-8.6547 16.98619,9.8699 v 19.740898 l -15.63409,9.085498 v 6.765401 a 2.9118515,2.9308623 0 0 0 0.10365,-0.058 l 19.89775,-11.5625 a 2.9118515,2.9308623 0 0 0 1.45483,-2.537801 v -23.124996 a 2.9118515,2.9308623 0 0 0 -1.45483,-2.5388 l -19.89775,-11.56251 a 2.9118515,2.9308623 0 0 0 -1.3873,-0.3918 z"
|
||||
id="path6666" />
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-feature-settings:normal;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;text-orientation:mixed;dominant-baseline:auto;baseline-shift:baseline;text-anchor:start;white-space:normal;shape-padding:0;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;vector-effect:none;fill:#21aba5;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:3.17499971;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:stroke fill markers;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
|
||||
d="m -569.00462,-38.844049 a 2.9118515,2.9308623 0 0 0 -0.77875,0.308999 l -19.89683,11.562501 a 2.9118515,2.9308623 0 0 0 -1.45672,2.538801 v 23.1250003 a 2.9118515,2.9308623 0 0 0 1.45672,2.537801 l 19.89683,11.5624977 a 2.9118515,2.9308623 0 0 0 2.91058,0 l 19.89775,-11.5624977 a 2.9118515,2.9308623 0 0 0 0.77778,-0.66460201 l -5.65379,-3.26999799 -16.47749,9.575199 -16.98435,-9.8709 V -22.74215 l 16.30827,-9.477299 z"
|
||||
id="path6668" />
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-feature-settings:normal;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;text-orientation:mixed;dominant-baseline:auto;baseline-shift:baseline;text-anchor:start;white-space:normal;shape-padding:0;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;vector-effect:none;fill:#21aba5;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:3.17499971;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:stroke fill markers;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
|
||||
d="m -460.9106,-37.293149 v 6.769201 l 13.39292,7.781798 v 19.7409023 l -16.9862,9.8709 -15.02174,-8.729901 -5.83733,3.375502 19.40327,11.2762977 a 2.9118515,2.9308623 0 0 0 2.91155,0 l 19.89678,-11.5624977 a 2.9118515,2.9308623 0 0 0 1.4558,-2.537801 V -24.433748 a 2.9118515,2.9308623 0 0 0 -1.4558,-2.538801 z"
|
||||
id="path6670" />
|
||||
</g>
|
||||
<path
|
||||
style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-feature-settings:normal;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;text-orientation:mixed;dominant-baseline:auto;baseline-shift:baseline;text-anchor:start;white-space:normal;shape-padding:0;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;vector-effect:none;fill:#21aba5;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:3.17500019;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
|
||||
d="m -531.46275,-116.06384 -0.48112,0.27957 v 5.56039 a 2.9213415,2.9213415 0 0 0 0.0171,0 h 33.93644 a 2.9213415,2.9213415 0 0 0 0.0181,-5.2e-4 v -5.55987 l -0.48109,-0.27957 z"
|
||||
id="path6674"
|
||||
inkscape:connector-curvature="0" />
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 24 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 51 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 80 KiB |
@@ -1,6 +0,0 @@
|
||||
{
|
||||
"watch": ["src"],
|
||||
"ext": "ts",
|
||||
"ignore": ["src/**/*.test.ts"],
|
||||
"exec": "ts-node -r tsconfig-paths/register --files src/server.ts"
|
||||
}
|
||||
+31
-28
@@ -1,18 +1,20 @@
|
||||
{
|
||||
"name": "turtle-cli",
|
||||
"version": "0.8.2",
|
||||
"version": "0.6.0",
|
||||
"description": "The command-line tool for building standalone Expo apps",
|
||||
"preferGlobal": true,
|
||||
"scripts": {
|
||||
"start": "EXPENV_OVERRIDES=local-common,local,local-queue expenv nodemon | bunyan -o short -l debug --color",
|
||||
"start": "EXPENV_OVERRIDES=local-common,local,local-queue expenv gulp",
|
||||
"start:ios": "PLATFORM=ios yarn start",
|
||||
"start:android": "PLATFORM=android yarn start",
|
||||
"watch": "EXPENV_OVERRIDES=local-common,local,local-queue expenv gulp watch:source",
|
||||
"bin:turtle": "./bin/turtle.js",
|
||||
"lint": "tslint -c tslint.json 'src/**/*.ts'",
|
||||
"lint:fix": "yarn lint --fix",
|
||||
"prepare": "rimraf ./build && yarn build",
|
||||
"build": "ttsc",
|
||||
"prepare": "yarn build",
|
||||
"build": "gulp build:server",
|
||||
"build:production": "./scripts/build.sh",
|
||||
"build:android-tarball:local": "./scripts/android/buildAndroidTarballLocally.sh",
|
||||
"secrets:init-private": "./scripts/initPrivateSecrets.sh",
|
||||
"set-workingdir:remote": "./scripts/setWorkingdirMode.sh remote",
|
||||
"set-workingdir:local": "./scripts/setWorkingdirMode.sh local",
|
||||
@@ -33,14 +35,9 @@
|
||||
"ssh:android:connect": "./scripts/ssh/connectToAndroidPod.sh",
|
||||
"ssh:ios:ips": "./scripts/ssh/findIosIps.sh",
|
||||
"set-priorities": "yarn set-priorities:local",
|
||||
"set-priorities:default": "expenv ts-node -r tsconfig-paths/register --files src/scripts/setConfigurations.ts",
|
||||
"set-priorities:local": "EXPENV_OVERRIDES=local-common,local,local-queue yarn set-priorities:default",
|
||||
"set-priorities:staging": "yarn set-priorities:default",
|
||||
"set-priorities:production": "EXPENV_OVERRIDES=prod yarn set-priorities:default",
|
||||
"set-replica-count:default": "expenv ts-node -r tsconfig-paths/register --files src/scripts/setReplicaCount.ts | bunyan -o short -l debug",
|
||||
"set-replica-count:local": "EXPENV_OVERRIDES=local-common,local,local-queue yarn set-replica-count:default",
|
||||
"set-replica-count:staging": "yarn set-replica-count:default",
|
||||
"set-replica-count:production": "EXPENV_OVERRIDES=prod yarn set-replica-count:default"
|
||||
"set-priorities:local": "EXPENV_OVERRIDES=local-common,local,local-queue expenv babel-node scripts/setConfigurations.js",
|
||||
"set-priorities:staging": "expenv babel-node scripts/setConfigurations.js",
|
||||
"set-priorities:production": "EXPENV_OVERRIDES=prod expenv babel-node scripts/setConfigurations.js"
|
||||
},
|
||||
"husky": {
|
||||
"hooks": {
|
||||
@@ -83,35 +80,42 @@
|
||||
"@types/fs-extra": "^5.0.4",
|
||||
"@types/jest": "^23.3.9",
|
||||
"@types/joi": "^13.4.3",
|
||||
"@types/lodash": "^4.14.129",
|
||||
"@types/lodash": "^4.14.109",
|
||||
"@types/progress": "^2.0.1",
|
||||
"@types/raven": "^2.5.1",
|
||||
"@types/request": "^2.47.1",
|
||||
"@types/semver": "^6.0.0",
|
||||
"@types/sharp": "^0.17.8",
|
||||
"@types/tar": "^4.0.0",
|
||||
"@types/uuid": "^3.4.3",
|
||||
"@types/which": "^1.3.1",
|
||||
"babel-cli": "^6.26.0",
|
||||
"babel-core": "^6.26.3",
|
||||
"babel-jest": "^23.6.0",
|
||||
"babel-plugin-module-resolver": "^3.1.1",
|
||||
"babel-preset-env": "^1.6.1",
|
||||
"gulp": "github:gulpjs/gulp#4.0",
|
||||
"gulp-babel": "^7.0.1",
|
||||
"gulp-changed": "^3.2.0",
|
||||
"gulp-nodemon": "^2.4.1",
|
||||
"gulp-plumber": "^1.2.0",
|
||||
"gulp-sourcemaps": "^2.6.4",
|
||||
"gulp-typescript": "^4.0.2",
|
||||
"husky": "^1.3.1",
|
||||
"jest": "^23.6.0",
|
||||
"lint-staged": "^8.1.5",
|
||||
"nock": "^10.0.1",
|
||||
"nodemon": "^1.18.11",
|
||||
"nodemon": "^1.18.7",
|
||||
"release-it": "^10.0.0",
|
||||
"rimraf": "^2.6.2",
|
||||
"source-map-loader": "^0.2.3",
|
||||
"ts-jest": "^23.10.4",
|
||||
"ts-node": "^8.0.3",
|
||||
"ts-transformer-imports": "^0.4.1",
|
||||
"tsconfig-paths": "^3.8.0",
|
||||
"tslint": "^5.10.0",
|
||||
"tslint-eslint-rules": "^5.4.0",
|
||||
"ttypescript": "^1.5.6",
|
||||
"typescript": "^3.4.2"
|
||||
"typescript": "^3.0.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@expo/spawn-async": "^1.4.2",
|
||||
"@expo/xdl": "54.1.4",
|
||||
"@google-cloud/logging-bunyan": "^0.9.5",
|
||||
"@expo/spawn-async": "^1.4.0",
|
||||
"@google-cloud/logging-bunyan": "^0.10.1",
|
||||
"async-retry": "^1.2.1",
|
||||
"aws-sdk": "^2.226.1",
|
||||
"bunyan": "^1.8.12",
|
||||
@@ -120,24 +124,23 @@
|
||||
"commander": "^2.15.1",
|
||||
"copy": "^0.3.1",
|
||||
"decompress": "^4.2.0",
|
||||
"dogapi": "^2.8.3",
|
||||
"fs-extra": "^6.0.0",
|
||||
"ioredis": "^3.2.2",
|
||||
"is-semver": "^1.0.8",
|
||||
"joi": "^13.2.0",
|
||||
"lodash": "^4.17.11",
|
||||
"lodash": "^4.17.10",
|
||||
"progress": "^2.0.0",
|
||||
"raven": "^2.6.2",
|
||||
"request": "^2.88.0",
|
||||
"request-progress": "^3.0.0",
|
||||
"semver": "^6.0.0",
|
||||
"sharp": "^0.21.0",
|
||||
"source-map-support": "^0.5.6",
|
||||
"tar": "^4.4.6",
|
||||
"uuid": "^3.2.1",
|
||||
"which": "^1.3.1"
|
||||
"which": "^1.3.1",
|
||||
"xdl": "53.1.6-alpha.2"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@expo/traveling-fastlane-darwin": "1.9.4"
|
||||
"@expo/traveling-fastlane-darwin": "^1.8.1"
|
||||
}
|
||||
}
|
||||
+32
@@ -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
|
||||
@@ -1,13 +1,12 @@
|
||||
|
||||
import { env } from 'turtle/utils/env';
|
||||
import { connect, RedisClient } from 'turtle/utils/redis';
|
||||
import { PLATFORMS } from 'turtle/constants';
|
||||
import logger from 'turtle/logger';
|
||||
import {
|
||||
createConfigurationsKey,
|
||||
createDefaultConfigurationKey,
|
||||
HIGH_CONFIGURATION,
|
||||
HIGH_ONLY_CONFIGURATION,
|
||||
NORMAL_CONFIGURATION,
|
||||
} from 'turtle/utils/priorities';
|
||||
import { connect, RedisClient } from 'turtle/utils/redis';
|
||||
|
||||
const TIMEOUT = 3000;
|
||||
|
||||
@@ -26,40 +25,36 @@ const TIMEOUT = 3000;
|
||||
// they should come back to using configurations no. 1 and 2.
|
||||
// If there are no more available configurations from the table, the default configuration is used.
|
||||
|
||||
|
||||
const AVAILABLE_CONFIGURATIONS_IOS = [
|
||||
HIGH_CONFIGURATION,
|
||||
HIGH_CONFIGURATION,
|
||||
HIGH_ONLY_CONFIGURATION,
|
||||
HIGH_ONLY_CONFIGURATION,
|
||||
HIGH_ONLY_CONFIGURATION,
|
||||
// there are 5 iOS builders at the moment
|
||||
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,
|
||||
HIGH_ONLY_CONFIGURATION,
|
||||
HIGH_ONLY_CONFIGURATION,
|
||||
HIGH_ONLY_CONFIGURATION,
|
||||
HIGH_ONLY_CONFIGURATION,
|
||||
HIGH_ONLY_CONFIGURATION,
|
||||
HIGH_ONLY_CONFIGURATION,
|
||||
HIGH_ONLY_CONFIGURATION,
|
||||
NORMAL_CONFIGURATION,
|
||||
NORMAL_CONFIGURATION,
|
||||
HIGH_CONFIGURATION,
|
||||
HIGH_CONFIGURATION,
|
||||
NORMAL_CONFIGURATION,
|
||||
NORMAL_CONFIGURATION,
|
||||
HIGH_CONFIGURATION,
|
||||
HIGH_CONFIGURATION,
|
||||
// there are 15 Android builders at the moment
|
||||
HIGH_CONFIGURATION,
|
||||
HIGH_CONFIGURATION,
|
||||
HIGH_CONFIGURATION,
|
||||
NORMAL_CONFIGURATION,
|
||||
NORMAL_CONFIGURATION,
|
||||
];
|
||||
|
||||
|
||||
const DEFAULT_CONFIGURATION = HIGH_CONFIGURATION;
|
||||
|
||||
async function run() {
|
||||
@@ -73,8 +68,6 @@ async function run() {
|
||||
redis.disconnect();
|
||||
}
|
||||
|
||||
if (require.main === module) {
|
||||
run()
|
||||
.then(() => logger.info('All done'))
|
||||
.catch((err) => logger.error('Error:', err));
|
||||
}
|
||||
run()
|
||||
.then(() => console.log('All done'))
|
||||
.catch(err => console.error('Error:', err));
|
||||
@@ -36,7 +36,7 @@ setup_local() {
|
||||
mkdir -p $WORKINGDIR
|
||||
echo "$WORKINGDIR directory created"
|
||||
|
||||
LINKS=("template-files" "package.json" "android" "expokit-npm-package" "packages" "client-builds" "cpp" "exponent-view-template" "ios" "shellAppBase-builds" "shellAppWorkspaces" "node_modules")
|
||||
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"
|
||||
@@ -55,18 +55,6 @@ setup_remote() {
|
||||
$ROOT_DIR/scripts/ios/fetchRemoteIosTarball.sh
|
||||
fi
|
||||
fi
|
||||
|
||||
for sdk in $ROOT_DIR/workingdir/android/* ; do
|
||||
if [[ -f "$sdk/universe-package.json" ]]; then
|
||||
mv $sdk/package.json $sdk/exponent-package.json
|
||||
mv $sdk/universe-package.json $sdk/package.json
|
||||
cd $sdk && yarn
|
||||
else
|
||||
if [[ -f "$sdk/package.json" ]]; then
|
||||
cd $sdk && yarn
|
||||
fi
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
switch_local() {
|
||||
|
||||
@@ -21,6 +21,5 @@ export API_HOSTNAME="staging.expo.io"
|
||||
export API_PORT="443"
|
||||
export REDIS_URL="noop"
|
||||
export REDIS_CONFIG_URL="noop"
|
||||
export DATADOG_DISABLED="true"
|
||||
|
||||
yarn test:smoke:android
|
||||
@@ -19,7 +19,6 @@ export API_HOSTNAME="staging.expo.io"
|
||||
export API_PORT="443"
|
||||
export REDIS_URL="noop"
|
||||
export REDIS_CONFIG_URL="noop"
|
||||
export DATADOG_DISABLED="true"
|
||||
|
||||
ROOT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )"/../.. && pwd )"
|
||||
SDK_VERSION=sdk32 $ROOT_DIR/scripts/ios/fetchRemoteIosTarball.sh
|
||||
|
||||
@@ -21,6 +21,5 @@ export API_HOSTNAME="noop"
|
||||
export API_PORT="443"
|
||||
export REDIS_URL="noop"
|
||||
export REDIS_CONFIG_URL="noop"
|
||||
export DATADOG_DISABLED="true"
|
||||
|
||||
yarn test:unit
|
||||
@@ -0,0 +1 @@
|
||||
s3://exp-artifacts/android-shell-builder-c19ce0d88a153f481fd790e23bdcd6f2d5ea7fd6.tar.gz
|
||||
@@ -1 +1 @@
|
||||
s3://exp-artifacts/android-shell-builder-f796aaffa0b5715079b2950faf26c2ff4e53e4c0.tar.gz
|
||||
s3://exp-artifacts/android-shell-builder-0cab68670fc50ce1dc16b7d86ca7976870062c39.tar.gz
|
||||
@@ -1 +0,0 @@
|
||||
s3://exp-artifacts/android-shell-builder-e244e865ed8cb8e20d29ea00b6373845d1c61068.tar.gz
|
||||
@@ -1 +1 @@
|
||||
s3://exp-artifacts/ios-expo-client-a2b1e16f330b36fa0dc627a76caabe4d2ff45f77.tar.gz
|
||||
s3://exp-artifacts/ios-expo-client-15bb883c22173d5b01df19421580170e776041d0.tar.gz
|
||||
@@ -0,0 +1 @@
|
||||
s3://exp-artifacts/ios-shell-builder-sdk-26-db356a09877642d64023af98e4e2e1e632fbe4df.tar.gz
|
||||
@@ -1 +0,0 @@
|
||||
s3://exp-artifacts/ios-shell-builder-sdk-latest-a2b1e16f330b36fa0dc627a76caabe4d2ff45f77.tar.gz
|
||||
@@ -34,7 +34,7 @@ async function performSmokeTest(recording: string) {
|
||||
});
|
||||
}
|
||||
|
||||
await jobManager.doJob();
|
||||
await jobManager.doJob(logger);
|
||||
if (!fixtureExists) {
|
||||
await fs.writeFile(recording, JSON.stringify(nock.recorder.play()));
|
||||
}
|
||||
|
||||
@@ -13,28 +13,3 @@ export const TYPE_DIMENSIONS = (type: string) => [
|
||||
{ Name: 'host', Value: config.hostname },
|
||||
],
|
||||
];
|
||||
|
||||
export const PRIORITY_DIMENSIONS = (type: string, priority: string) => [
|
||||
[
|
||||
{ Name: 'env', Value: config.deploymentEnv },
|
||||
{ Name: 'platform', Value: config.platform },
|
||||
{ Name: 'priority', Value: priority },
|
||||
],
|
||||
[
|
||||
{ Name: 'env', Value: config.deploymentEnv },
|
||||
{ Name: 'platform', Value: config.platform },
|
||||
{ Name: 'type', Value: type },
|
||||
],
|
||||
[
|
||||
{ Name: 'env', Value: config.deploymentEnv },
|
||||
{ Name: 'platform', Value: config.platform },
|
||||
{ Name: 'type', Value: type },
|
||||
{ Name: 'priority', Value: priority },
|
||||
],
|
||||
[
|
||||
{ Name: 'env', Value: config.deploymentEnv },
|
||||
{ Name: 'platform', Value: config.platform },
|
||||
{ Name: 'type', Value: type },
|
||||
{ Name: 'host', Value: config.hostname },
|
||||
],
|
||||
];
|
||||
+19
-18
@@ -11,7 +11,6 @@ import {
|
||||
MetricRegistrationObject,
|
||||
} from 'turtle/aws/cloudwatch/types';
|
||||
import config from 'turtle/config';
|
||||
import logger from 'turtle/logger';
|
||||
|
||||
interface IState {
|
||||
metrics: IMetric[];
|
||||
@@ -38,12 +37,12 @@ const cloudWatch = new aws.CloudWatch({
|
||||
secretAccessKey: config.aws.secretAccessKey,
|
||||
});
|
||||
|
||||
export function init() {
|
||||
export function init(logger: any) {
|
||||
if (config.cloudwatch.disabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
state.intervalId = setInterval(_dumpMetrics, config.cloudwatch.intervalMs);
|
||||
state.intervalId = setInterval(_dumpMetrics(logger), config.cloudwatch.intervalMs);
|
||||
}
|
||||
|
||||
export function addMetricData({
|
||||
@@ -111,23 +110,25 @@ function _calculateGroupHash(metricName: string, extraDimensions: any[]) {
|
||||
return `${metricName}${dimString}`;
|
||||
}
|
||||
|
||||
function _dumpMetrics() {
|
||||
if (!state.metrics.length && Object.keys(state.registeredMetrics).length < 0) {
|
||||
// empty metrics list, skipping
|
||||
return;
|
||||
}
|
||||
function _dumpMetrics(logger: any) {
|
||||
return () => {
|
||||
if (!state.metrics.length && Object.keys(state.registeredMetrics).length < 0) {
|
||||
// empty metrics list, skipping
|
||||
return;
|
||||
}
|
||||
|
||||
logger.trace(
|
||||
'[cloudwatch] lenghts for state.metrics=%d, state.registeredMetrics=%d',
|
||||
state.metrics.length,
|
||||
Object.keys(state.registeredMetrics).length,
|
||||
);
|
||||
logger.trace(
|
||||
'[cloudwatch] lengths for state.metrics=%d, state.registeredMetrics=%d',
|
||||
state.metrics.length,
|
||||
Object.keys(state.registeredMetrics).length,
|
||||
);
|
||||
|
||||
_addEmptyMetrics(state.metrics);
|
||||
const formattedMetrics = _formatMetrics(state.metrics);
|
||||
_pushMetrics(formattedMetrics);
|
||||
_addEmptyMetrics(state.metrics);
|
||||
const formattedMetrics = _formatMetrics(state.metrics);
|
||||
_pushMetrics(formattedMetrics, logger);
|
||||
|
||||
state.metrics = [];
|
||||
state.metrics = [];
|
||||
};
|
||||
}
|
||||
|
||||
function _addEmptyMetrics(metrics: IMetric[]) {
|
||||
@@ -174,7 +175,7 @@ function _reduceMetrics(metrics: IMetric[]) {
|
||||
);
|
||||
}
|
||||
|
||||
function _pushMetrics(metrics: IMetricsChunk[]) {
|
||||
function _pushMetrics(metrics: IMetricsChunk[], logger: any) {
|
||||
logger.trace('[cloudwatch] pushing metrics', JSON.stringify(metrics, null, '\t'));
|
||||
metrics.forEach((metricsChunk) => {
|
||||
cloudWatch.putMetricData(metricsChunk, (err) => {
|
||||
|
||||
+3
-4
@@ -5,7 +5,6 @@ import _ from 'lodash';
|
||||
import { OUTPUT_QUEUE_URL, QUEUE_URL } from 'turtle/aws/utils';
|
||||
import config from 'turtle/config';
|
||||
import { JOB_UPDATE_TYPE } from 'turtle/constants/build';
|
||||
import logger from 'turtle/logger';
|
||||
import { getCurrentJobId } from 'turtle/turtleContext';
|
||||
|
||||
const sqs = new AWS.SQS({
|
||||
@@ -16,7 +15,7 @@ const sqs = new AWS.SQS({
|
||||
|
||||
const VISIBILITY_TIMEOUT_SEC = 30;
|
||||
|
||||
export async function receiveMessage(priority: string) {
|
||||
export async function receiveMessage(priority: string, logger: any) {
|
||||
const params = {
|
||||
MaxNumberOfMessages: 1,
|
||||
QueueUrl: QUEUE_URL(priority),
|
||||
@@ -64,7 +63,7 @@ export async function changeMessageVisibility(priority: string, receiptHandle: s
|
||||
// Every VISIBILITY_TIMEOUT_SEC / 3 seconds we are telling AWS SQS
|
||||
// that we're still processing the message, so it does not
|
||||
// send the build job to another turtle agent
|
||||
export function changeMessageVisibilityRecurring(priority: string, receiptHandle: string, jobId: string) {
|
||||
export function changeMessageVisibilityRecurring(priority: string, receiptHandle: string, jobId: string, logger: any) {
|
||||
return setInterval(() => {
|
||||
if (getCurrentJobId() === jobId) {
|
||||
changeMessageVisibility(priority, receiptHandle).catch((err) => {
|
||||
@@ -74,7 +73,7 @@ export function changeMessageVisibilityRecurring(priority: string, receiptHandle
|
||||
}, (VISIBILITY_TIMEOUT_SEC * 1000) / 3);
|
||||
}
|
||||
|
||||
export async function sendMessage(jobId: string, status: JOB_UPDATE_TYPE, data = {}) {
|
||||
export async function sendMessage(jobId: string, status: JOB_UPDATE_TYPE, data = {}, logger: any) {
|
||||
const params = {
|
||||
MessageBody: JSON.stringify({ jobId, status, ...data }),
|
||||
QueueUrl: OUTPUT_QUEUE_URL(),
|
||||
|
||||
@@ -15,18 +15,11 @@ export default (program: any, setCommonCommandOptions: any) => {
|
||||
.option(
|
||||
'--keystore-path <app.jks>',
|
||||
'path to your Keystore (please provide Keystore password and Key password as EXPO_ANDROID_KEYSTORE_PASSWORD'
|
||||
+ ' and EXPO_ANDROID_KEY_PASSWORD env variables)',
|
||||
+ 'and EXPO_ANDROID_KEY_PASSWORD env variables)',
|
||||
)
|
||||
.option('--keystore-alias <alias>', 'keystore Alias')
|
||||
.option(
|
||||
'-t --type <build>',
|
||||
'type of build: app-bundle|apk',
|
||||
/^(app-bundle|apk)$/i,
|
||||
'app-bundle',
|
||||
)
|
||||
.description(
|
||||
'Build a standalone APK or App Bundle for your project, signed and ready for submission to the Google Play'
|
||||
+ ' Store.',
|
||||
'Build a standalone APK for your project, signed and ready for submission to the Google Play Store.',
|
||||
)
|
||||
.asyncAction(
|
||||
createBuilderAction({
|
||||
@@ -41,17 +34,11 @@ export default (program: any, setCommonCommandOptions: any) => {
|
||||
);
|
||||
};
|
||||
|
||||
const buildJobObject = (
|
||||
appJSON: any,
|
||||
{ releaseChannel, username, projectDir, publicUrl, buildType }: any,
|
||||
credentials: any,
|
||||
) => ({
|
||||
const buildJobObject = (appJSON: any, { releaseChannel, username, projectDir, publicUrl }: any, credentials: any) => ({
|
||||
config: {
|
||||
..._.get(appJSON, 'expo.android.config', {}),
|
||||
releaseChannel,
|
||||
androidPackage: _.get(appJSON, 'expo.android.package'),
|
||||
publicUrl,
|
||||
buildType,
|
||||
},
|
||||
id: uuid.v4(),
|
||||
platform: PLATFORMS.ANDROID,
|
||||
|
||||
@@ -2,8 +2,8 @@ import { default as buildAndroid } from 'turtle/bin/commands/build/android';
|
||||
import { default as buildIOS } from 'turtle/bin/commands/build/ios';
|
||||
|
||||
function createCommand(builder: any) {
|
||||
return (program: any) => {
|
||||
return builder(program, setCommonCommandOptions);
|
||||
return (logger: any) => (program: any) => {
|
||||
return builder(program, setCommonCommandOptions, logger);
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import uuid from 'uuid';
|
||||
import { ErrorWithCommandHelp } from 'turtle/bin/commands/ErrorWithCommandHelp';
|
||||
import { createBuilderAction } from 'turtle/bin/utils/builder';
|
||||
import builder from 'turtle/builders/ios';
|
||||
import { IOS_BUILD_TYPES, PLATFORMS } from 'turtle/constants';
|
||||
import { IOS, PLATFORMS } from 'turtle/constants';
|
||||
|
||||
export default (program: any, setCommonCommandOptions: any) => {
|
||||
const command = program.command('build:ios [project-dir]');
|
||||
@@ -56,7 +56,7 @@ const buildJobObject = (appJSON: any, { releaseChannel, buildType, username, pub
|
||||
});
|
||||
|
||||
const prepareCredentials = async (cmd: any) => {
|
||||
if (cmd.type !== IOS_BUILD_TYPES.ARCHIVE) {
|
||||
if (cmd.type !== IOS.BUILD_TYPES.ARCHIVE) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import builders from 'turtle/bin/commands/build';
|
||||
import setup from 'turtle/bin/commands/setup';
|
||||
|
||||
export const setupIOS = setup.ios;
|
||||
export const setupAndroid = setup.android;
|
||||
|
||||
export const buildIOS = builders.ios;
|
||||
export const buildAndroid = builders.android;
|
||||
export default {
|
||||
setupIOS: setup.ios,
|
||||
setupAndroid: setup.android,
|
||||
buildIOS: builders.ios,
|
||||
buildAndroid: builders.android,
|
||||
};
|
||||
@@ -4,9 +4,6 @@ import { ErrorWithCommandHelp } from 'turtle/bin/commands/ErrorWithCommandHelp';
|
||||
import { ErrorWithProgramHelp } from 'turtle/bin/commands/ErrorWithProgramHelp';
|
||||
import setup from 'turtle/bin/setup/setup';
|
||||
import { PLATFORMS } from 'turtle/constants';
|
||||
import logger from 'turtle/logger';
|
||||
|
||||
const l = logger.child({ buildPhase: 'setting up environment' });
|
||||
|
||||
function createSetupCommand(platform: string, os?: string) {
|
||||
return (program: any) => {
|
||||
@@ -23,14 +20,16 @@ function createSetupCommand(platform: string, os?: string) {
|
||||
};
|
||||
}
|
||||
|
||||
async function setupAction(program: any, cmd: any, platform: string, os?: string) {
|
||||
async function setupAction(program: any, cmd: any, platform: string, logger: any, os?: string) {
|
||||
try {
|
||||
if (os && process.platform !== os) {
|
||||
throw new Error('We don\'t support running standalone app builds for this platform on your operating system');
|
||||
}
|
||||
|
||||
const { sdkVersion } = cmd;
|
||||
await setup(platform, sdkVersion);
|
||||
const l = logger.child({ buildPhase: 'setting up environment' });
|
||||
|
||||
await setup(platform, l, sdkVersion);
|
||||
l.info('it\'s all set!');
|
||||
} catch (err) {
|
||||
logger.error({ err }, `Failed to setup environment for ${platform} builds`);
|
||||
|
||||
+3
-3
@@ -1,10 +1,10 @@
|
||||
import path from 'path';
|
||||
|
||||
import { LoggerDetach, ModuleVersion } from '@expo/xdl';
|
||||
import program, { Command } from 'commander';
|
||||
import fs from 'fs-extra';
|
||||
import { LoggerDetach, ModuleVersion } from 'xdl';
|
||||
|
||||
import * as commands from 'turtle/bin/commands';
|
||||
import commands from 'turtle/bin/commands';
|
||||
import logger from 'turtle/logger';
|
||||
|
||||
const { name, version } = JSON.parse(fs.readFileSync(path.join(__dirname, '../../package.json'), 'utf-8'));
|
||||
@@ -33,7 +33,7 @@ export function run(programName: string) {
|
||||
|
||||
async function runAsync(programName: string) {
|
||||
program.version(version);
|
||||
Object.values(commands).forEach((command) => registerCommand(program, command));
|
||||
Object.values(commands).map((command) => command(logger)).forEach((command) => registerCommand(program, command));
|
||||
program.parse(process.argv);
|
||||
|
||||
const subCommand = process.argv[2];
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import path from 'path';
|
||||
import util from 'util';
|
||||
|
||||
import { ExponentTools } from '@expo/xdl';
|
||||
import fs from 'fs-extra';
|
||||
import _which from 'which';
|
||||
import { ExponentTools } from 'xdl';
|
||||
|
||||
import ensureAndroidNDKIsPresent from 'turtle/bin/setup/android/ndk';
|
||||
import ensureAndroidSDKIsPresent from 'turtle/bin/setup/android/sdk';
|
||||
@@ -12,7 +12,6 @@ import { IToolDefinition } from 'turtle/bin/setup/utils/toolsDetector';
|
||||
import { formatShellAppDirectory } from 'turtle/builders/utils/android/workingdir';
|
||||
import config from 'turtle/config';
|
||||
import { PLATFORMS } from 'turtle/constants';
|
||||
import logger from 'turtle/logger';
|
||||
|
||||
const which = util.promisify(_which);
|
||||
const REQUIRED_TOOLS: IToolDefinition[] = [
|
||||
@@ -40,25 +39,24 @@ const REQUIRED_TOOLS: IToolDefinition[] = [
|
||||
},
|
||||
},
|
||||
];
|
||||
const LOGGER_FIELDS = { buildPhase: 'setting up environment' };
|
||||
const l = logger.child(LOGGER_FIELDS);
|
||||
|
||||
export default async function setup(sdkVersion?: string) {
|
||||
await checkSystem(REQUIRED_TOOLS);
|
||||
await prepareAndroidEnv();
|
||||
export default async function setup(logger: any, sdkVersion?: string) {
|
||||
await checkSystem(REQUIRED_TOOLS, logger);
|
||||
await prepareAndroidEnv(logger);
|
||||
if (sdkVersion) {
|
||||
await ensureShellAppIsPresent(
|
||||
sdkVersion,
|
||||
{ formatShellAppDirectory, formatShellAppTarballUriPath },
|
||||
logger,
|
||||
_shellAppPostDownloadAction,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async function prepareAndroidEnv() {
|
||||
async function prepareAndroidEnv(logger: any) {
|
||||
await fs.ensureDir(config.directories.androidDependenciesDir);
|
||||
const sdkConfig = await ensureAndroidSDKIsPresent();
|
||||
const ndkConfig = await ensureAndroidNDKIsPresent();
|
||||
const sdkConfig = await ensureAndroidSDKIsPresent(logger);
|
||||
const ndkConfig = await ensureAndroidNDKIsPresent(logger);
|
||||
_setEnvVars({ ...sdkConfig.envVars, ...ndkConfig.envVars });
|
||||
_alterPath([...sdkConfig.path, ...sdkConfig.path]);
|
||||
}
|
||||
@@ -67,29 +65,23 @@ function formatShellAppTarballUriPath(sdkMajorVersion: string) {
|
||||
return path.join(config.directories.shellTarballsDir, PLATFORMS.ANDROID, `sdk${sdkMajorVersion}`);
|
||||
}
|
||||
|
||||
async function _shellAppPostDownloadAction(sdkVersion: string, workingdir: string) {
|
||||
async function _shellAppPostDownloadAction(workingdir: string, logger: any) {
|
||||
const inWorkingdir = (filename: string) => path.join(workingdir, filename);
|
||||
|
||||
if (await fs.pathExists(inWorkingdir('universe-package.json'))) {
|
||||
// legacy shell app built from universe
|
||||
await fs.move(inWorkingdir('package.json'), inWorkingdir('exponent-package.json'));
|
||||
await fs.move(inWorkingdir('universe-package.json'), inWorkingdir('package.json'));
|
||||
await _installNodeModules(workingdir);
|
||||
await fs.move(inWorkingdir('package.json'), inWorkingdir('universe-package.json'));
|
||||
await fs.move(inWorkingdir('exponent-package.json'), inWorkingdir('package.json'));
|
||||
} else {
|
||||
// new shell app built from expo or not
|
||||
await _installNodeModules(workingdir);
|
||||
}
|
||||
await fs.move(inWorkingdir('package.json'), inWorkingdir('exponent-package.json'));
|
||||
await fs.move(inWorkingdir('universe-package.json'), inWorkingdir('package.json'));
|
||||
await _installNodeModules(workingdir, logger);
|
||||
await fs.move(inWorkingdir('package.json'), inWorkingdir('universe-package.json'));
|
||||
await fs.move(inWorkingdir('exponent-package.json'), inWorkingdir('package.json'));
|
||||
|
||||
// TODO: remove this after making sure that we don't need node_modules in tools-public for older sdks
|
||||
if (ExponentTools.parseSdkMajorVersion(sdkVersion) < 33) {
|
||||
const toolsPublicDir = path.join(workingdir, 'tools-public');
|
||||
await _installNodeModules(toolsPublicDir);
|
||||
}
|
||||
// TODO: remove following lines after upgrading android shell app
|
||||
const toolsPublicDir = path.join(workingdir, 'tools-public');
|
||||
await _installNodeModules(toolsPublicDir, logger);
|
||||
}
|
||||
|
||||
async function _installNodeModules(cwd: string) {
|
||||
async function _installNodeModules(cwd: string, logger: any) {
|
||||
const LOGGER_FIELDS = { buildPhase: 'setting up environment' };
|
||||
const l = logger.child(LOGGER_FIELDS);
|
||||
l.info(`installing dependencies in ${cwd} directory...`);
|
||||
const command = await _shouldUseYarnOrNpm();
|
||||
await ExponentTools.spawnAsyncThrowError(command, ['install'], {
|
||||
|
||||
@@ -7,20 +7,17 @@ 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.child(LOGGER_FIELDS);
|
||||
|
||||
export default async function ensureAndroidNDKIsPresent() {
|
||||
export default async function ensureAndroidNDKIsPresent(logger: any) {
|
||||
const androidNdkDir = path.join(config.directories.androidDependenciesDir, 'ndk');
|
||||
if (!(await fs.pathExists(androidNdkDir))) {
|
||||
await fs.ensureDir(androidNdkDir);
|
||||
const androidNdkDownloadPath = formatArtifactDownloadPath(ANDROID_NDK_URL);
|
||||
|
||||
try {
|
||||
const l = logger.child({ buildPhase: 'setting up environment' });
|
||||
l.info('Downloading Android NDK');
|
||||
await fs.ensureDir(config.directories.artifactsDir);
|
||||
await download(ANDROID_NDK_URL, androidNdkDownloadPath);
|
||||
|
||||
@@ -1,27 +1,24 @@
|
||||
import os from 'os';
|
||||
import path from 'path';
|
||||
|
||||
import { ExponentTools } from '@expo/xdl';
|
||||
import decompress from 'decompress';
|
||||
import fs from 'fs-extra';
|
||||
import { ExponentTools } from 'xdl';
|
||||
|
||||
import { formatArtifactDownloadPath } from 'turtle/bin/setup/utils/common';
|
||||
import download from 'turtle/bin/setup/utils/downloader';
|
||||
import config from 'turtle/config';
|
||||
import logger from 'turtle/logger';
|
||||
|
||||
const ANDROID_SDK_URL = 'https://dl.google.com/android/repository/sdk-tools-linux-4333796.zip';
|
||||
|
||||
const LOGGER_FIELDS = { buildPhase: 'setting up environment' };
|
||||
const l = logger.child(LOGGER_FIELDS);
|
||||
|
||||
export default async function ensureAndroidSDKIsPresent() {
|
||||
export default async function ensureAndroidSDKIsPresent(logger: any) {
|
||||
const androidSdkDir = path.join(config.directories.androidDependenciesDir, 'sdk');
|
||||
if (!(await fs.pathExists(androidSdkDir))) {
|
||||
await fs.ensureDir(androidSdkDir);
|
||||
const androidSdkDownloadPath = formatArtifactDownloadPath(ANDROID_SDK_URL);
|
||||
|
||||
try {
|
||||
const l = logger.child(LOGGER_FIELDS);
|
||||
l.info('Downloading Android SDK');
|
||||
await fs.ensureDir(config.directories.artifactsDir);
|
||||
await download(ANDROID_SDK_URL, androidSdkDownloadPath);
|
||||
|
||||
+4
-30
@@ -1,9 +1,6 @@
|
||||
import last from 'lodash/last';
|
||||
import trim from 'lodash/trim';
|
||||
import path from 'path';
|
||||
import semver from 'semver';
|
||||
|
||||
import { ExponentTools } from '@expo/xdl';
|
||||
import { ExponentTools } from 'xdl';
|
||||
|
||||
import { checkSystem, ensureShellAppIsPresent } from 'turtle/bin/setup/utils/common';
|
||||
import { IToolDefinition } from 'turtle/bin/setup/utils/toolsDetector';
|
||||
@@ -19,21 +16,6 @@ const REQUIRED_TOOLS: IToolDefinition[] = [
|
||||
{
|
||||
command: 'fastlane',
|
||||
missingDescription: 'Please check https://docs.fastlane.tools/getting-started/ios/setup/',
|
||||
versionCheckFn: async () => {
|
||||
const MINIMAL_VERSION = '2.99.0';
|
||||
|
||||
const { stdout } = await ExponentTools.spawnAsyncThrowError(
|
||||
'fastlane',
|
||||
['--version'],
|
||||
{ stdio: 'pipe', env: { ...process.env, FASTLANE_SKIP_UPDATE_CHECK: '1', LC_ALL: 'en_US.UTF-8' } },
|
||||
);
|
||||
const fastlaneVersion = parseFastlaneVersion(stdout);
|
||||
if (fastlaneVersion && !semver.satisfies(fastlaneVersion, `>= ${MINIMAL_VERSION}`)) {
|
||||
throw new Error(
|
||||
`Your fastlane on version ${fastlaneVersion}. Please upgrade it to at least ${MINIMAL_VERSION}.`,
|
||||
);
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
command: 'xcode',
|
||||
@@ -52,24 +34,16 @@ const REQUIRED_TOOLS: IToolDefinition[] = [
|
||||
},
|
||||
];
|
||||
|
||||
export default async function setup(sdkVersion?: string) {
|
||||
await checkSystem(REQUIRED_TOOLS);
|
||||
export default async function setup(logger: any, sdkVersion?: string) {
|
||||
await checkSystem(REQUIRED_TOOLS, logger);
|
||||
if (sdkVersion) {
|
||||
await ensureShellAppIsPresent(sdkVersion, {
|
||||
formatShellAppDirectory,
|
||||
formatShellAppTarballUriPath,
|
||||
});
|
||||
}, logger);
|
||||
}
|
||||
}
|
||||
|
||||
function formatShellAppTarballUriPath(sdkMajorVersion: string) {
|
||||
return path.join(config.directories.shellTarballsDir, PLATFORMS.IOS, `sdk${sdkMajorVersion}`);
|
||||
}
|
||||
|
||||
function parseFastlaneVersion(stdout: string) {
|
||||
const lastLine = last(trim(stdout).split('\n'));
|
||||
if (!lastLine) {
|
||||
return null;
|
||||
}
|
||||
return last(lastLine.split(' ')) || null;
|
||||
}
|
||||
@@ -4,15 +4,15 @@ import setupAndroid from 'turtle/bin/setup/android';
|
||||
import setupIos from 'turtle/bin/setup/ios';
|
||||
import { PLATFORMS } from 'turtle/constants';
|
||||
|
||||
export default async function setup(platform: string, sdkVersion?: string) {
|
||||
export default async function setup(platform: string, logger: any, sdkVersion?: string) {
|
||||
if (sdkVersion && !isSemver(sdkVersion)) {
|
||||
throw new Error('SDK version is not valid.');
|
||||
}
|
||||
|
||||
if (platform === PLATFORMS.IOS) {
|
||||
return await setupIos(sdkVersion);
|
||||
return await setupIos(logger, sdkVersion);
|
||||
} else if (platform === PLATFORMS.ANDROID) {
|
||||
return await setupAndroid(sdkVersion);
|
||||
return await setupAndroid(logger, sdkVersion);
|
||||
} else {
|
||||
throw new Error('This should never happen :(');
|
||||
}
|
||||
|
||||
@@ -1,57 +1,61 @@
|
||||
import path from 'path';
|
||||
|
||||
import { ExponentTools } from '@expo/xdl';
|
||||
import fs from 'fs-extra';
|
||||
import tar from 'tar';
|
||||
import { ExponentTools } from 'xdl';
|
||||
|
||||
import download from 'turtle/bin/setup/utils/downloader';
|
||||
import { ensureToolsAreInstalled, IToolDefinition } from 'turtle/bin/setup/utils/toolsDetector';
|
||||
import { IShellAppDirectoryConfig } from 'turtle/builders/utils/workingdir';
|
||||
import config from 'turtle/config';
|
||||
import logger from 'turtle/logger';
|
||||
|
||||
const l = logger.child({ buildPhase: 'setting up environment' });
|
||||
|
||||
interface IShellAppFormaters {
|
||||
formatShellAppDirectory: (config: IShellAppDirectoryConfig) => string;
|
||||
formatShellAppTarballUriPath: (sdkMajorVersion: string) => string;
|
||||
}
|
||||
|
||||
type PostDownloadAction = (sdkVersion: string, workingdir: string) => void;
|
||||
type PostDownloadAction = (workingdir: string, logger: any) => void;
|
||||
|
||||
export async function checkSystem(requiredTools: IToolDefinition[]) {
|
||||
await ensureToolsAreInstalled(requiredTools);
|
||||
export async function checkSystem(requiredTools: IToolDefinition[], logger: any) {
|
||||
await ensureToolsAreInstalled(requiredTools, logger);
|
||||
}
|
||||
|
||||
export async function ensureShellAppIsPresent(
|
||||
sdkVersion: string,
|
||||
formatters: IShellAppFormaters,
|
||||
logger: any,
|
||||
postDownloadAction?: PostDownloadAction,
|
||||
) {
|
||||
const workingdir = formatters.formatShellAppDirectory({ sdkVersion });
|
||||
const l = logger.child({ buildPhase: 'setting up environment' });
|
||||
if (await fs.pathExists(workingdir)) {
|
||||
return;
|
||||
}
|
||||
l.info(`shell app for SDK ${sdkVersion} doesn't exist, downloading...`);
|
||||
await _downloadShellApp(sdkVersion, workingdir, formatters);
|
||||
await _downloadShellApp(sdkVersion, workingdir, formatters, l);
|
||||
if (postDownloadAction) {
|
||||
await postDownloadAction(sdkVersion, workingdir);
|
||||
await postDownloadAction(workingdir, logger);
|
||||
}
|
||||
}
|
||||
|
||||
async function _downloadShellApp(sdkVersion: string, targetDirectory: string, formatters: IShellAppFormaters) {
|
||||
async function _downloadShellApp(
|
||||
sdkVersion: string,
|
||||
targetDirectory: string,
|
||||
formatters: IShellAppFormaters,
|
||||
logger: any,
|
||||
) {
|
||||
const shellAppTarballS3Uri = await _readShellAppTarballS3Uri(sdkVersion, formatters);
|
||||
const tarballDownloadTargetPath = formatArtifactDownloadPath(shellAppTarballS3Uri);
|
||||
await fs.ensureDir(config.directories.artifactsDir);
|
||||
await download(shellAppTarballS3Uri, tarballDownloadTargetPath);
|
||||
l.info('shell app has been downloaded');
|
||||
logger.info('shell app has been downloaded');
|
||||
await fs.ensureDir(targetDirectory);
|
||||
l.info('extracting shell app (this may take a while)...');
|
||||
logger.info('extracting shell app (this may take a while)...');
|
||||
await tar.x({
|
||||
file: tarballDownloadTargetPath,
|
||||
C: targetDirectory,
|
||||
});
|
||||
l.info('shell app extracted');
|
||||
logger.info('shell app extracted');
|
||||
}
|
||||
|
||||
export function formatArtifactDownloadPath(uri: string) {
|
||||
|
||||
@@ -1,21 +1,17 @@
|
||||
import util from 'util';
|
||||
import _which from 'which';
|
||||
|
||||
import logger from 'turtle/logger';
|
||||
|
||||
const which = util.promisify(_which);
|
||||
const l = logger.child({ buildPhase: 'setting up environment' });
|
||||
|
||||
export interface IToolDefinition {
|
||||
command: string;
|
||||
missingDescription: string;
|
||||
testFn?: () => Promise<boolean>;
|
||||
versionCheckFn?: () => void;
|
||||
}
|
||||
|
||||
export async function ensureToolsAreInstalled(tools: IToolDefinition[]) {
|
||||
export async function ensureToolsAreInstalled(tools: IToolDefinition[], logger: any) {
|
||||
let isAnyToolMissing = false;
|
||||
for (const { command, missingDescription, testFn, versionCheckFn } of tools) {
|
||||
for (const { command, missingDescription, testFn } of tools) {
|
||||
try {
|
||||
if (testFn) {
|
||||
if (!await testFn()) {
|
||||
@@ -25,20 +21,13 @@ export async function ensureToolsAreInstalled(tools: IToolDefinition[]) {
|
||||
await which(command);
|
||||
}
|
||||
} catch (err) {
|
||||
const l = logger.child({ buildPhase: 'setting up environment' });
|
||||
isAnyToolMissing = true;
|
||||
if (!testFn) {
|
||||
l.error({ err }, `${command} is missing in your $PATH`);
|
||||
}
|
||||
l.error({ err }, missingDescription);
|
||||
}
|
||||
if (versionCheckFn) {
|
||||
try {
|
||||
await versionCheckFn();
|
||||
} catch (err) {
|
||||
isAnyToolMissing = true;
|
||||
l.error({ err }, `wrong version of ${command} installed`);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (isAnyToolMissing) {
|
||||
throw new Error('Tools required to perform standalone app build are missing. Please install them.');
|
||||
|
||||
@@ -5,10 +5,10 @@ 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({
|
||||
logger,
|
||||
program,
|
||||
command,
|
||||
prepareCredentials,
|
||||
@@ -62,7 +62,7 @@ export function createBuilderAction({
|
||||
|
||||
const appJSON = await ProjectUtils.loadAppJSON(projectDirArg, cmd.config);
|
||||
const sdkVersion = _.get(appJSON, 'expo.sdkVersion');
|
||||
await setup(platform, sdkVersion);
|
||||
await setup(platform, logger, sdkVersion);
|
||||
const credentials = await prepareCredentials(cmd);
|
||||
const rawJob = {
|
||||
...buildJobObject(appJSON, args, credentials),
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { User as UserManager } from '@expo/xdl';
|
||||
import { User as UserManager } from 'xdl';
|
||||
|
||||
import { ErrorWithProgramHelp } from 'turtle/bin/commands/ErrorWithProgramHelp';
|
||||
|
||||
|
||||
+10
-23
@@ -1,36 +1,30 @@
|
||||
import path from 'path';
|
||||
|
||||
import { AndroidShellApp, ImageUtils } from '@expo/xdl';
|
||||
import fs from 'fs-extra';
|
||||
import _ from 'lodash';
|
||||
import semver from 'semver';
|
||||
import { ANDROID_BUILD_TYPES } from 'turtle/constants';
|
||||
import uuidv4 from 'uuid/v4';
|
||||
import { AndroidShellApp, ImageUtils } from 'xdl';
|
||||
|
||||
import getOrCreateCredentials from 'turtle/builders/utils/android/credentials';
|
||||
import { formatShellAppDirectory } from 'turtle/builders/utils/android/workingdir';
|
||||
import * as commonUtils from 'turtle/builders/utils/common';
|
||||
import * as imageHelpers from 'turtle/builders/utils/image';
|
||||
import { resolveNativeModules } from 'turtle/builders/utils/unimodules';
|
||||
import { uploadBuildToS3 } from 'turtle/builders/utils/uploader';
|
||||
import { ensureCanBuildSdkVersion } from 'turtle/builders/utils/version';
|
||||
import config from 'turtle/config';
|
||||
import { IAndroidCredentials, IJob, IJobResult } from 'turtle/job';
|
||||
import logger from 'turtle/logger';
|
||||
|
||||
export default async function buildAndroid(jobData: IJob): Promise<IJobResult> {
|
||||
export default async function buildAndroid(jobData: IJob, logger: any): Promise<IJobResult> {
|
||||
await ensureCanBuildSdkVersion(jobData);
|
||||
const credentials = await getOrCreateCredentials(jobData);
|
||||
const outputFilePath = await runShellAppBuilder(jobData, credentials);
|
||||
|
||||
const credentials = await getOrCreateCredentials(jobData, logger);
|
||||
const apkFilePath = await runShellAppBuilder(jobData, credentials, logger);
|
||||
const randomHex = uuidv4().replace(/-/g, '');
|
||||
const s3FileExtension = jobData.config.buildType === ANDROID_BUILD_TYPES.APP_BUNDLE ? 'aab' : 'apk';
|
||||
const s3Filename = `${jobData.experienceName}-${randomHex}-signed.${s3FileExtension}`;
|
||||
const s3Filename = `${jobData.experienceName}-${randomHex}-signed.apk`;
|
||||
const s3FileKey = `android/${s3Filename}`;
|
||||
const fakeUploadFilename = s3Filename.replace('/', '\\');
|
||||
|
||||
const artifactUrl = await uploadBuildToS3({
|
||||
uploadPath: outputFilePath,
|
||||
logger,
|
||||
uploadPath: apkFilePath,
|
||||
s3FileKey,
|
||||
...config.builder.fakeUpload && {
|
||||
fakeUploadBuildPath:
|
||||
@@ -46,6 +40,7 @@ export default async function buildAndroid(jobData: IJob): Promise<IJobResult> {
|
||||
async function runShellAppBuilder(
|
||||
jobData: IJob,
|
||||
credentials: IAndroidCredentials,
|
||||
logger: any,
|
||||
): Promise<string> {
|
||||
const { temporaryFilesRoot } = config.directories;
|
||||
await fs.ensureDir(temporaryFilesRoot);
|
||||
@@ -66,18 +61,12 @@ async function runShellAppBuilder(
|
||||
ImageUtils.setResizeImageFunction(imageHelpers.resizeIconWithSharpAsync);
|
||||
ImageUtils.setGetImageDimensionsFunction(imageHelpers.getImageDimensionsWithSharpAsync);
|
||||
|
||||
const fileExtension = jobData.config.buildType === ANDROID_BUILD_TYPES.APP_BUNDLE ? 'aab' : 'apk';
|
||||
const outputFilePath = path.join(temporaryFilesRoot, `shell-signed-${jobData.id}.${fileExtension}`);
|
||||
const outputFilePath = path.join(temporaryFilesRoot, `shell-signed-${jobData.id}.apk`);
|
||||
|
||||
const { config: jobConfig, manifest, sdkVersion: sdkVersionFromJob } = jobData;
|
||||
const sdkVersion = _.get(manifest, 'sdkVersion', sdkVersionFromJob);
|
||||
const workingDir = formatShellAppDirectory({ sdkVersion });
|
||||
|
||||
logger.info({ buildPhase: 'resolve native modules' }, 'Resolving universal modules dependencies');
|
||||
const enabledModules = semver.satisfies(sdkVersion, '>= 33.0.0')
|
||||
? await resolveNativeModules(workingDir, manifest && manifest.dependencies)
|
||||
: null;
|
||||
|
||||
try {
|
||||
await AndroidShellApp.createAndroidShellAppAsync({
|
||||
url: commonUtils.getExperienceUrl(jobData),
|
||||
@@ -91,11 +80,9 @@ async function runShellAppBuilder(
|
||||
releaseChannel: jobConfig.releaseChannel,
|
||||
workingDir,
|
||||
outputFile: outputFilePath,
|
||||
modules: enabledModules,
|
||||
buildType: jobData.config.buildType,
|
||||
});
|
||||
} catch (err) {
|
||||
commonUtils.logErrorOnce(err);
|
||||
commonUtils.logErrorOnce(err, logger);
|
||||
throw err;
|
||||
} finally {
|
||||
if (!config.builder.skipCleanup) {
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import { IJob, IJobResult } from 'turtle/job';
|
||||
import buildAndroid from './android';
|
||||
import buildIOS from './ios';
|
||||
|
||||
export default {
|
||||
const builders: { [index: string]: (job: IJob, logger: any) => Promise<IJobResult>} = {
|
||||
ios: buildIOS,
|
||||
android: buildAndroid,
|
||||
};
|
||||
|
||||
export default builders;
|
||||
@@ -1,5 +1,5 @@
|
||||
import { IosIPABuilder as createIPABuilder } from '@expo/xdl';
|
||||
import _ from 'lodash';
|
||||
import { IosIPABuilder as createIPABuilder } from 'xdl';
|
||||
|
||||
import { IContext } from 'turtle/builders/ios/context';
|
||||
import { logErrorOnce } from 'turtle/builders/utils/common';
|
||||
@@ -7,7 +7,6 @@ import * as fileUtils from 'turtle/builders/utils/file';
|
||||
import * as keychain from 'turtle/builders/utils/ios/keychain';
|
||||
import runShellAppBuilder from 'turtle/builders/utils/ios/shellAppBuilder';
|
||||
import { IJob } from 'turtle/job';
|
||||
import logger from 'turtle/logger/index';
|
||||
|
||||
export default async function buildArchive(ctx: IContext, job: IJob) {
|
||||
let keychainInfo;
|
||||
@@ -19,7 +18,7 @@ export default async function buildArchive(ctx: IContext, job: IJob) {
|
||||
const manifest = await runShellAppBuilder(ctx, job);
|
||||
await buildAndSignIPA(ctx, job, keychainInfo.path, manifest);
|
||||
} catch (err) {
|
||||
logErrorOnce(err);
|
||||
logErrorOnce(err, ctx.logger);
|
||||
throw err;
|
||||
} finally {
|
||||
if (keychainInfo) {
|
||||
@@ -29,8 +28,8 @@ export default async function buildArchive(ctx: IContext, job: IJob) {
|
||||
}
|
||||
|
||||
async function buildAndSignIPA(ctx: IContext, job: IJob, keychainPath: string, manifest: any) {
|
||||
const l = logger.child({ buildPhase: 'building and signing IPA' });
|
||||
l.info('building and signing IPA');
|
||||
ctx.logger = ctx.logger.child({ buildPhase: 'building and signing IPA' });
|
||||
ctx.logger.info('building and signing IPA');
|
||||
|
||||
const {
|
||||
credentials: { provisioningProfile, certPassword, teamId },
|
||||
@@ -42,7 +41,7 @@ async function buildAndSignIPA(ctx: IContext, job: IJob, keychainPath: string, m
|
||||
|
||||
const { provisioningProfilePath } = ctx;
|
||||
await fileUtils.writeBase64ToBinaryFile(provisioningProfilePath, provisioningProfile as string);
|
||||
l.info('saved provisioning profile to temporary path');
|
||||
ctx.logger.info('saved provisioning profile to temporary path');
|
||||
|
||||
const ipaBuilder = createIPABuilder({
|
||||
keychainPath,
|
||||
@@ -57,5 +56,5 @@ async function buildAndSignIPA(ctx: IContext, job: IJob, keychainPath: string, m
|
||||
});
|
||||
await ipaBuilder.build();
|
||||
|
||||
l.info(`done building and signing IPA`);
|
||||
ctx.logger.info(`done building and signing IPA`);
|
||||
}
|
||||
@@ -1,11 +1,11 @@
|
||||
import * as path from 'path';
|
||||
|
||||
import { IosShellApp } from '@expo/xdl';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { IosShellApp } from 'xdl';
|
||||
|
||||
import { formatShellAppDirectory } from 'turtle/builders/utils/ios/workingdir';
|
||||
import config from 'turtle/config';
|
||||
import { IOS_BUILD_TYPES, PLATFORMS } from 'turtle/constants/index';
|
||||
import { IOS, PLATFORMS } from 'turtle/constants/index';
|
||||
import { IJob } from 'turtle/job';
|
||||
|
||||
export interface IContext {
|
||||
@@ -15,6 +15,7 @@ export interface IContext {
|
||||
baseArchiveDir: string;
|
||||
buildDir: string;
|
||||
fakeUploadBuildPath?: string;
|
||||
logger: any;
|
||||
outputPath: string;
|
||||
provisioningProfilePath: string;
|
||||
provisioningProfileDir: string;
|
||||
@@ -27,8 +28,9 @@ export interface IContext {
|
||||
}
|
||||
|
||||
const { EXPOKIT_APP, EXPONENT_APP } = IosShellApp;
|
||||
const { BUILD_TYPES } = IOS;
|
||||
|
||||
export function createBuilderContext(job: IJob): IContext {
|
||||
export function createBuilderContext(job: IJob, logger: any): IContext {
|
||||
const { join } = path;
|
||||
const appUUID = uuidv4();
|
||||
const {
|
||||
@@ -46,6 +48,7 @@ export function createBuilderContext(job: IJob): IContext {
|
||||
appDir: join(config.directories.temporaryFilesRoot, appUUID),
|
||||
appUUID,
|
||||
workingDir,
|
||||
logger,
|
||||
};
|
||||
context.buildDir = join(context.appDir, 'build');
|
||||
context.provisioningProfileDir = join(context.appDir, 'provisioning');
|
||||
@@ -56,7 +59,7 @@ export function createBuilderContext(job: IJob): IContext {
|
||||
context.tempCertPath = join(context.appDir, 'cert.p12');
|
||||
context.baseArchiveDir = join(context.appDir, 'archive');
|
||||
|
||||
if (buildType === IOS_BUILD_TYPES.CLIENT) {
|
||||
if (buildType === BUILD_TYPES.CLIENT) {
|
||||
context.applicationFilesSrc = join(
|
||||
workingDir,
|
||||
'expo-client-build',
|
||||
@@ -73,7 +76,7 @@ export function createBuilderContext(job: IJob): IContext {
|
||||
);
|
||||
}
|
||||
|
||||
if (buildType === IOS_BUILD_TYPES.ARCHIVE) {
|
||||
if (buildType === BUILD_TYPES.ARCHIVE) {
|
||||
context.outputPath = join(context.appDir, 'archive.xcarchive');
|
||||
context.uploadPath = join(context.buildDir, 'archive.ipa');
|
||||
context.archiveDir = join(
|
||||
@@ -91,7 +94,7 @@ export function createBuilderContext(job: IJob): IContext {
|
||||
'default',
|
||||
`${EXPOKIT_APP}.xcworkspace`,
|
||||
);
|
||||
} else if (buildType === IOS_BUILD_TYPES.CLIENT) {
|
||||
} else if (buildType === BUILD_TYPES.CLIENT) {
|
||||
context.outputPath = join(context.appDir, 'archive.xcarchive');
|
||||
context.uploadPath = join(context.buildDir, 'archive.ipa');
|
||||
context.archiveDir = join(
|
||||
@@ -106,7 +109,7 @@ export function createBuilderContext(job: IJob): IContext {
|
||||
'ios',
|
||||
`${EXPONENT_APP}.xcworkspace`,
|
||||
);
|
||||
} else if (buildType === IOS_BUILD_TYPES.SIMULATOR) {
|
||||
} else if (buildType === BUILD_TYPES.SIMULATOR) {
|
||||
context.outputPath = join(context.appDir, 'archive.tar.gz');
|
||||
context.uploadPath = join(context.appDir, 'archive.tar.gz');
|
||||
context.archiveDir = join(
|
||||
@@ -116,7 +119,7 @@ export function createBuilderContext(job: IJob): IContext {
|
||||
);
|
||||
}
|
||||
|
||||
const s3FileExtension = buildType === IOS_BUILD_TYPES.SIMULATOR ? 'tar.gz' : 'ipa';
|
||||
const s3FileExtension = buildType === BUILD_TYPES.SIMULATOR ? 'tar.gz' : 'ipa';
|
||||
const s3Filename = `${job.experienceName}-${appUUID}-${buildType}.${s3FileExtension}`;
|
||||
if (config.builder.fakeUpload) {
|
||||
const fakeUploadFilename = s3Filename.replace('/', '\\');
|
||||
|
||||
@@ -9,28 +9,30 @@ import prepareAdHocBuildCredentials from 'turtle/builders/utils/ios/adhocBuild';
|
||||
import { uploadBuildToS3 } from 'turtle/builders/utils/uploader';
|
||||
import { ensureCanBuildSdkVersion } from 'turtle/builders/utils/version';
|
||||
import config from 'turtle/config';
|
||||
import { IOS_BUILD_TYPES } from 'turtle/constants';
|
||||
import { IOS } from 'turtle/constants/index';
|
||||
import { IJob, IJobResult } from 'turtle/job';
|
||||
|
||||
export default async function iosBuilder(job: IJob): Promise<IJobResult> {
|
||||
if (job.config.buildType !== IOS_BUILD_TYPES.CLIENT) {
|
||||
const { BUILD_TYPES } = IOS;
|
||||
|
||||
export default async function iosBuilder(job: IJob, logger: any): Promise<IJobResult> {
|
||||
if (job.config.buildType !== BUILD_TYPES.CLIENT) {
|
||||
await ensureCanBuildSdkVersion(job);
|
||||
}
|
||||
|
||||
const ctx = createBuilderContext(job);
|
||||
const ctx = createBuilderContext(job, logger);
|
||||
|
||||
try {
|
||||
await initBuilder(ctx);
|
||||
|
||||
const { buildType } = job.config;
|
||||
|
||||
if (buildType === IOS_BUILD_TYPES.CLIENT) {
|
||||
await prepareAdHocBuildCredentials(job);
|
||||
if (buildType === BUILD_TYPES.CLIENT) {
|
||||
await prepareAdHocBuildCredentials(job, ctx.logger);
|
||||
}
|
||||
|
||||
if ([IOS_BUILD_TYPES.ARCHIVE, IOS_BUILD_TYPES.CLIENT].includes(buildType!)) {
|
||||
if ([BUILD_TYPES.ARCHIVE, BUILD_TYPES.CLIENT].includes(buildType!)) {
|
||||
await buildArchive(ctx, job);
|
||||
} else if (buildType === IOS_BUILD_TYPES.SIMULATOR) {
|
||||
} else if (buildType === BUILD_TYPES.SIMULATOR) {
|
||||
await buildSimulator(ctx, job);
|
||||
} else {
|
||||
throw new Error(`Unsupported iOS build type: ${buildType}`);
|
||||
@@ -39,7 +41,7 @@ export default async function iosBuilder(job: IJob): Promise<IJobResult> {
|
||||
const artifactUrl = await uploadBuildToS3(ctx);
|
||||
return { artifactUrl };
|
||||
} catch (err) {
|
||||
logErrorOnce(err);
|
||||
logErrorOnce(err, ctx.logger);
|
||||
throw err;
|
||||
} finally {
|
||||
await cleanup(ctx);
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import { IContext } from 'turtle/builders/ios/context';
|
||||
import runShellAppBuilder from 'turtle/builders/utils/ios/shellAppBuilder';
|
||||
import { IJob } from 'turtle/job';
|
||||
import logger from 'turtle/logger';
|
||||
|
||||
export default async function buildSimulator(ctx: IContext, job: IJob) {
|
||||
const l = logger.child({ buildPhase: 'running simulator builder' });
|
||||
l.info('running simulator build');
|
||||
ctx.logger = ctx.logger.child({ buildPhase: 'running simulator builder' });
|
||||
ctx.logger.info('running simulator build');
|
||||
await runShellAppBuilder(ctx, job);
|
||||
l.info('build complete');
|
||||
ctx.logger.info('build complete');
|
||||
}
|
||||
@@ -1,17 +1,15 @@
|
||||
import path from 'path';
|
||||
|
||||
import { Credentials } from '@expo/xdl';
|
||||
import fs from 'fs-extra';
|
||||
import get from 'lodash/get';
|
||||
import uuidv4 from 'uuid/v4';
|
||||
import { Credentials } from 'xdl';
|
||||
|
||||
import * as sqs from 'turtle/aws/sqs';
|
||||
import { UPDATE_CREDENTIALS } from 'turtle/constants/build';
|
||||
import { IAndroidCredentials, IJob } from 'turtle/job';
|
||||
import logger from 'turtle/logger';
|
||||
import { isOffline } from 'turtle/turtleContext';
|
||||
|
||||
async function getOrCreateCredentials(jobData: IJob): Promise<IAndroidCredentials> {
|
||||
async function getOrCreateCredentials(jobData: IJob, logger: any): Promise<IAndroidCredentials> {
|
||||
if (!jobData.credentials) {
|
||||
const l = logger.child({ buildPhase: 'generating keystore' });
|
||||
const credentials: any = {};
|
||||
@@ -20,14 +18,7 @@ async function getOrCreateCredentials(jobData: IJob): Promise<IAndroidCredential
|
||||
credentials.keystorePassword = uuidv4().replace(/-/g, '');
|
||||
credentials.keyPassword = uuidv4().replace(/-/g, '');
|
||||
credentials.keystoreAlias = Buffer.from(jobData.experienceName).toString('base64');
|
||||
const androidPackage =
|
||||
get(jobData, 'manifest.android.package')
|
||||
|| get(jobData, 'config.androidPackage');
|
||||
if (!androidPackage) {
|
||||
throw new Error(
|
||||
'Android package name is not set in the app manifest (please update app.json if you\'re using turtle-cli).',
|
||||
);
|
||||
}
|
||||
const androidPackage = jobData.manifest.android.package;
|
||||
|
||||
await Credentials.Android.createKeystore(
|
||||
{
|
||||
@@ -52,7 +43,7 @@ async function getOrCreateCredentials(jobData: IJob): Promise<IAndroidCredential
|
||||
} else {
|
||||
await sqs.sendMessage(jobData.id, UPDATE_CREDENTIALS, {
|
||||
...credentials,
|
||||
});
|
||||
}, l);
|
||||
l.info('Keystore sent to storage successfully');
|
||||
}
|
||||
await fs.unlink(keystoreFilename);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import path from 'path';
|
||||
|
||||
import { ExponentTools } from '@expo/xdl';
|
||||
import { ExponentTools } from 'xdl';
|
||||
|
||||
import { IShellAppDirectoryConfig } from 'turtle/builders/utils/workingdir';
|
||||
import config from 'turtle/config';
|
||||
|
||||
@@ -2,7 +2,6 @@ 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;
|
||||
@@ -18,7 +17,7 @@ export function getExperienceUrl(job: IJob) {
|
||||
|
||||
const alreadyLoggedError = Symbol('alreadyLoggedError');
|
||||
|
||||
export function logErrorOnce(err: any) {
|
||||
export function logErrorOnce(err: any, logger: any) {
|
||||
if (!err[alreadyLoggedError]) {
|
||||
logger.error(err.stack);
|
||||
err[alreadyLoggedError] = true;
|
||||
|
||||
@@ -1,21 +1,20 @@
|
||||
import path from 'path';
|
||||
|
||||
import spawnAsync from '@expo/spawn-async';
|
||||
import { IosCodeSigning } from '@expo/xdl';
|
||||
import fs from 'fs-extra';
|
||||
import isEmpty from 'lodash/isEmpty';
|
||||
import { IosCodeSigning } from 'xdl';
|
||||
|
||||
import * as sqs from 'turtle/aws/sqs';
|
||||
import BuildError, { BuildErrorReason } from 'turtle/builders/BuildError';
|
||||
import { UPDATE_CREDENTIALS } from 'turtle/constants/build';
|
||||
import { IJob } from 'turtle/job';
|
||||
import logger from 'turtle/logger';
|
||||
import { isOffline } from 'turtle/turtleContext';
|
||||
|
||||
// tslint:disable-next-line:no-var-requires
|
||||
const travelingFastlane = process.platform === 'darwin' ? require('@expo/traveling-fastlane-darwin')() : null;
|
||||
|
||||
async function prepareAdHocBuildCredentials(job: IJob) {
|
||||
async function prepareAdHocBuildCredentials(job: IJob, logger: any) {
|
||||
if (process.platform !== 'darwin') {
|
||||
throw new Error('This function should be called only on macOS!');
|
||||
}
|
||||
@@ -59,7 +58,7 @@ async function prepareAdHocBuildCredentials(job: IJob) {
|
||||
await sqs.sendMessage(job.id, UPDATE_CREDENTIALS, {
|
||||
provisioningProfileId: credentials.provisioningProfileId,
|
||||
provisioningProfile: credentials.provisioningProfile,
|
||||
});
|
||||
}, logger);
|
||||
logger.info('Ad Hoc provisioning profile sent to storage successfully');
|
||||
}
|
||||
} catch (e) {
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
import { IosKeychain } from '@expo/xdl';
|
||||
import { IosKeychain } from 'xdl';
|
||||
|
||||
import { IContext } from 'turtle/builders/ios/context';
|
||||
import * as fileUtils from 'turtle/builders/utils/file';
|
||||
import logger from 'turtle/logger';
|
||||
|
||||
interface IKeychain {
|
||||
path: string;
|
||||
}
|
||||
|
||||
export async function create(ctx: IContext): Promise<IKeychain> {
|
||||
const l = logger.child({ buildPhase: 'creating keychain' });
|
||||
const l = ctx.logger.child({ buildPhase: 'creating keychain' });
|
||||
try {
|
||||
l.info('creating keychain...');
|
||||
const keychainInfo = await IosKeychain.createKeychain(ctx.appUUID, false);
|
||||
@@ -22,7 +21,7 @@ export async function create(ctx: IContext): Promise<IKeychain> {
|
||||
}
|
||||
|
||||
export async function remove(ctx: IContext, keychainPath: string) {
|
||||
const l = logger.child({ buildPhase: 'deleting keychain' });
|
||||
const l = ctx.logger.child({ buildPhase: 'deleting keychain' });
|
||||
try {
|
||||
l.info('delete keychain...');
|
||||
const keychainInfo = await IosKeychain.deleteKeychain({
|
||||
@@ -49,7 +48,7 @@ export async function importCert(
|
||||
certPassword,
|
||||
}: { keychainPath: string; certP12: string; certPassword: string },
|
||||
) {
|
||||
const l = logger.child({ buildPhase: 'importing certificate into keychain' });
|
||||
const l = ctx.logger.child({ buildPhase: 'importing certificate into keychain' });
|
||||
try {
|
||||
l.info('importing distribution certificate into keychain...');
|
||||
const { tempCertPath: certPath } = ctx;
|
||||
|
||||
@@ -1,16 +1,15 @@
|
||||
import path from 'path';
|
||||
import util from 'util';
|
||||
|
||||
import { ImageUtils, IosShellApp } from '@expo/xdl';
|
||||
import copy from 'copy';
|
||||
import _ from 'lodash';
|
||||
import { ImageUtils, IosShellApp } from 'xdl';
|
||||
|
||||
import { IContext } from 'turtle/builders/ios/context';
|
||||
import * as commonUtils from 'turtle/builders/utils/common';
|
||||
import * as imageHelpers from 'turtle/builders/utils/image';
|
||||
import { IOS_BUILD_TYPES } from 'turtle/constants/index';
|
||||
import { IOS } from 'turtle/constants/index';
|
||||
import { IJob } from 'turtle/job';
|
||||
import logger from 'turtle/logger/index';
|
||||
|
||||
const copyAsync = util.promisify(copy);
|
||||
|
||||
@@ -21,7 +20,7 @@ export default async function runShellAppBuilder(ctx: IContext, job: IJob): Prom
|
||||
|
||||
await copyAsync(ctx.applicationFilesSrc, ctx.baseArchiveDir);
|
||||
|
||||
logger.info(
|
||||
ctx.logger.info(
|
||||
{ buildPhase: 'icons setup' },
|
||||
'ImageUtils: setting image functions to alternative sharp implementations',
|
||||
);
|
||||
@@ -37,7 +36,7 @@ export default async function runShellAppBuilder(ctx: IContext, job: IJob): Prom
|
||||
output: ctx.outputPath,
|
||||
verbose: true,
|
||||
};
|
||||
if (buildType === IOS_BUILD_TYPES.CLIENT) {
|
||||
if (buildType === IOS.BUILD_TYPES.CLIENT) {
|
||||
Object.assign(shellAppParams, {
|
||||
workspacePath: path.join(ctx.workingDir, 'ios'),
|
||||
});
|
||||
@@ -49,6 +48,6 @@ export default async function runShellAppBuilder(ctx: IContext, job: IJob): Prom
|
||||
});
|
||||
}
|
||||
|
||||
logger.info({ buildPhase: 'configuring NSBundle' }, 'configuring NSBundle...');
|
||||
ctx.logger.info({ buildPhase: 'configuring NSBundle' }, 'configuring NSBundle...');
|
||||
return await IosShellApp.configureAndCopyArchiveAsync(shellAppParams);
|
||||
}
|
||||
@@ -1,14 +1,14 @@
|
||||
import path from 'path';
|
||||
|
||||
import { ExponentTools } from '@expo/xdl';
|
||||
import { ExponentTools } from 'xdl';
|
||||
|
||||
import { IShellAppDirectoryConfig } from 'turtle/builders/utils/workingdir';
|
||||
import config from 'turtle/config';
|
||||
import { IOS_BUILD_TYPES, PLATFORMS } from 'turtle/constants';
|
||||
import { IOS, PLATFORMS } from 'turtle/constants';
|
||||
|
||||
export function formatShellAppDirectory({ sdkVersion, buildType }: IShellAppDirectoryConfig) {
|
||||
const { workingDir } = config.directories;
|
||||
if (buildType === IOS_BUILD_TYPES.CLIENT) {
|
||||
if (buildType === IOS.BUILD_TYPES.CLIENT) {
|
||||
return path.join(workingDir, PLATFORMS.IOS, 'client');
|
||||
} else if (config.builder.useLocalWorkingDir) {
|
||||
return path.join(workingDir, 'local');
|
||||
|
||||
@@ -1,169 +0,0 @@
|
||||
import fs from 'fs-extra';
|
||||
import filter from 'lodash/filter';
|
||||
import keys from 'lodash/keys';
|
||||
import merge from 'lodash/merge';
|
||||
import values from 'lodash/values';
|
||||
import path from 'path';
|
||||
import logger from 'turtle/logger';
|
||||
|
||||
export interface IUnimoduleEntry {
|
||||
name: string;
|
||||
version: string;
|
||||
dirname: string;
|
||||
}
|
||||
|
||||
interface IPackageInfo {
|
||||
name: string;
|
||||
version: string;
|
||||
dirname: string; // could be different than name
|
||||
dependencies: string[];
|
||||
}
|
||||
|
||||
export async function resolveNativeModules(workingdir: string, packages?: string[]): Promise<IUnimoduleEntry[]> {
|
||||
const resolver = new Resolver(workingdir);
|
||||
await resolver.init();
|
||||
|
||||
if (!packages) {
|
||||
resolver.addAll();
|
||||
} else {
|
||||
const corePackages = await readCorePackages(workingdir);
|
||||
for (const pkg of corePackages) {
|
||||
if (resolver.isUnimodule(pkg)) {
|
||||
resolver.addModule(pkg);
|
||||
}
|
||||
}
|
||||
for (const pkg of packages) {
|
||||
if (resolver.isUnimodule(pkg)) {
|
||||
resolver.addModule(pkg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return resolver.getModules();
|
||||
}
|
||||
|
||||
async function readCorePackages(workingdir: string) {
|
||||
const pkgJsonPath = path.join(workingdir, 'node_modules', 'react-native-unimodules', 'package.json');
|
||||
const expoPkgJsonPath = path.join(workingdir, 'packages', 'expo', 'package.json');
|
||||
if (await fs.pathExists(pkgJsonPath) && await fs.pathExists(expoPkgJsonPath)) {
|
||||
const packageJson = JSON.parse(await fs.readFile(pkgJsonPath, 'utf-8'));
|
||||
const expoPackageJson = JSON.parse(await fs.readFile(expoPkgJsonPath, 'utf-8'));
|
||||
return keys(packageJson.dependencies).concat(keys(expoPackageJson.dependencies));
|
||||
} else {
|
||||
logger.warn('No core packages detected');
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
interface IDependencyInfo {
|
||||
[key: string]: IPackageInfo;
|
||||
}
|
||||
|
||||
class Resolver {
|
||||
private workingdir: string;
|
||||
private modulesMap: {[key: string]: IUnimoduleEntry};
|
||||
private dependencyMap: IDependencyInfo;
|
||||
|
||||
constructor(workingdir: string) {
|
||||
this.workingdir = workingdir;
|
||||
this.modulesMap = {};
|
||||
this.dependencyMap = {};
|
||||
}
|
||||
|
||||
public async init() {
|
||||
this.dependencyMap = await generateDependenciesInfo(this.workingdir);
|
||||
}
|
||||
|
||||
public isUnimodule(moduleName: string): boolean {
|
||||
return !!this.dependencyMap[moduleName];
|
||||
}
|
||||
|
||||
public addAll() {
|
||||
for (const moduleName of Object.keys(this.dependencyMap)) {
|
||||
this.addModule(moduleName);
|
||||
}
|
||||
}
|
||||
|
||||
public addModule(moduleName: string) {
|
||||
if (this.dependencyMap[moduleName]) {
|
||||
this.resolveModule(moduleName);
|
||||
} else {
|
||||
throw new Error('Attempting to add unknown module');
|
||||
}
|
||||
}
|
||||
|
||||
public getModules(): IUnimoduleEntry[] {
|
||||
return values(this.modulesMap);
|
||||
}
|
||||
|
||||
private async resolveModule(moduleName: string) {
|
||||
if (this.modulesMap[moduleName]) {
|
||||
return;
|
||||
}
|
||||
const pkg = this.dependencyMap[moduleName];
|
||||
logger.info(`Adding ${moduleName}:${pkg.version}`);
|
||||
this.modulesMap[moduleName] = {
|
||||
name: pkg.name,
|
||||
dirname: pkg.dirname,
|
||||
version: pkg.version,
|
||||
};
|
||||
for (const dep of pkg.dependencies) {
|
||||
this.resolveModule(dep);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function generateDependenciesInfo(workingdir: string): Promise<IDependencyInfo> {
|
||||
const dir = await fs.readdir(path.join(workingdir, 'packages'));
|
||||
const map: IDependencyInfo = {};
|
||||
|
||||
// scrap directory for package metadata
|
||||
for (const pkgName of dir) {
|
||||
const pkg = await getPackage(workingdir, pkgName);
|
||||
if (pkg) {
|
||||
map[pkg.name] = pkg;
|
||||
}
|
||||
}
|
||||
|
||||
// handle @unimodules directory
|
||||
const unimodulesDir = await fs.readdir(path.join(workingdir, 'packages', '@unimodules'));
|
||||
for (const pkgName of unimodulesDir) {
|
||||
const pkg = await getPackage(workingdir, path.join('@unimodules', pkgName));
|
||||
if (pkg) {
|
||||
map[pkg.name] = pkg;
|
||||
}
|
||||
}
|
||||
|
||||
// remove non unimodule dependencies
|
||||
for (const pkg of Object.keys(map)) {
|
||||
map[pkg].dependencies = filter(map[pkg].dependencies, (dep) => !!map[dep]);
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
async function getPackage(workingdir: string, pkgName: string): Promise<IPackageInfo | null> {
|
||||
const pkgPath = path.join(workingdir, 'packages', pkgName);
|
||||
const isAndroid = await fs.pathExists(path.join(pkgPath, 'android'));
|
||||
const isIos = await fs.pathExists(path.join(pkgPath, 'ios'));
|
||||
const isUnimodule = await fs.pathExists(path.join(pkgPath, 'unimodule.json'));
|
||||
const hasPackageJson = await fs.pathExists(path.join(pkgPath, 'package.json'));
|
||||
if (!isUnimodule || !hasPackageJson || (!isAndroid && !isIos)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const packageJson = JSON.parse(await fs.readFile(path.join(pkgPath, 'package.json'), 'utf-8'));
|
||||
|
||||
const pkgDeps = merge({}, // reading everything to make sure
|
||||
packageJson.dependencies,
|
||||
packageJson.peerDependencies,
|
||||
packageJson.unimodulePeerDependencies,
|
||||
);
|
||||
const info: IPackageInfo = {
|
||||
name: packageJson.name,
|
||||
version: packageJson.version,
|
||||
dirname: pkgName,
|
||||
dependencies: Object.keys(pkgDeps),
|
||||
};
|
||||
|
||||
return info;
|
||||
}
|
||||
@@ -2,24 +2,24 @@ import * as fs from 'fs-extra';
|
||||
|
||||
import { uploadFile } from 'turtle/aws/s3';
|
||||
import config from 'turtle/config';
|
||||
import logger from 'turtle/logger';
|
||||
|
||||
interface IUploadCtx {
|
||||
fakeUploadBuildPath?: string;
|
||||
s3FileKey?: string;
|
||||
uploadPath: string;
|
||||
logger: any;
|
||||
}
|
||||
|
||||
export async function uploadBuildToS3(ctx: IUploadCtx) {
|
||||
if (config.builder.fakeUpload) {
|
||||
const l = logger.child({ buildPhase: 'copying build artifact' });
|
||||
const l = ctx.logger.child({ buildPhase: 'copying build artifact' });
|
||||
const { fakeUploadBuildPath, uploadPath } = ctx;
|
||||
l.info('copying build to fake upload directory');
|
||||
await fs.copy(uploadPath, fakeUploadBuildPath as string);
|
||||
l.info(`copied build to ${fakeUploadBuildPath}`);
|
||||
return fakeUploadBuildPath as string;
|
||||
} else {
|
||||
const l = logger.child({ buildPhase: 'uploading to S3' });
|
||||
const l = ctx.logger.child({ buildPhase: 'uploading to S3' });
|
||||
l.info('uploading build artifact to S3');
|
||||
const { Location: fileLocation } = await uploadFile({
|
||||
key: ctx.s3FileKey as string,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { ExponentTools } from '@expo/xdl';
|
||||
import fs from 'fs-extra';
|
||||
import path from 'path';
|
||||
import { ExponentTools } from 'xdl';
|
||||
|
||||
import config from 'turtle/config';
|
||||
import { IJob } from 'turtle/job';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { ANDROID_BUILD_TYPES, IOS_BUILD_TYPES } from 'turtle/constants';
|
||||
import { IOS } from 'turtle/constants';
|
||||
|
||||
export interface IShellAppDirectoryConfig {
|
||||
sdkVersion: string;
|
||||
buildType?: IOS_BUILD_TYPES & ANDROID_BUILD_TYPES;
|
||||
buildType?: IOS.BUILD_TYPES;
|
||||
}
|
||||
+6
-9
@@ -2,7 +2,7 @@ import os from 'os';
|
||||
import path from 'path';
|
||||
|
||||
import { PLATFORMS } from 'turtle/constants';
|
||||
import { env, envBoolean, envNum, envOptional, envTransform } from 'turtle/utils/env';
|
||||
import { env, envNum, envOptional, envTransform } from 'turtle/utils/env';
|
||||
|
||||
export default {
|
||||
env: env('NODE_ENV'),
|
||||
@@ -37,19 +37,16 @@ export default {
|
||||
intervalMs: envNum('AWS_CLOUDWATCH_INTERVAL_MS', 30000),
|
||||
namespace: env('AWS_CLOUDWATCH_NAMESPACE', 'Turtle'),
|
||||
},
|
||||
datadog: {
|
||||
disabled: envBoolean('DATADOG_DISABLED', false),
|
||||
apiKey: envOptional('DATADOG_API_KEY'),
|
||||
appKey: envOptional('DATADOG_APP_KEY'),
|
||||
intervalMs: env('DATADOG_INTERVAL_MS', 5 * 60 * 1000),
|
||||
},
|
||||
redis: {
|
||||
url: env('REDIS_URL'),
|
||||
configUrl: env('REDIS_CONFIG_URL'),
|
||||
},
|
||||
logger: {
|
||||
level: env('LOGGER_LEVEL', 'info'),
|
||||
intervalMs: envNum('LOGGER_INTERVAL_MS', 5000),
|
||||
interval: envNum('LOGGER_INTERVAL', 5),
|
||||
client: {
|
||||
level: env('CLIENT_LOGGER_LEVEL', 'info'),
|
||||
},
|
||||
},
|
||||
google: {
|
||||
credentials: env('GOOGLE_APPLICATION_CREDENTIALS', ''),
|
||||
@@ -70,7 +67,7 @@ export default {
|
||||
(val: string) => env('NODE_ENV') === 'development' && val === '1',
|
||||
),
|
||||
fakeUpload: envTransform('TURTLE_FAKE_UPLOAD', '0', (val: string) => val === '1'),
|
||||
maxJobTimeMs: envNum('TURTLE_MAX_JOB_TIME_MS', 15 * 60 * 1000),
|
||||
maxJobTimeMs: envNum('TURTLE_MAX_JOB_TIME_MS', 60 * 60 * 1000),
|
||||
useLocalWorkingDir: envTransform('TURTLE_USE_LOCAL_WORKING_DIR', '0', (val: string) => val === '1'),
|
||||
},
|
||||
directories: {
|
||||
|
||||
+2
-12
@@ -1,19 +1,9 @@
|
||||
import * as BUILD from './build';
|
||||
import * as IOS from './ios';
|
||||
|
||||
export enum PLATFORMS {
|
||||
IOS = 'ios',
|
||||
ANDROID = 'android',
|
||||
}
|
||||
|
||||
export enum IOS_BUILD_TYPES {
|
||||
ARCHIVE = 'archive',
|
||||
CLIENT = 'client',
|
||||
SIMULATOR = 'simulator',
|
||||
}
|
||||
|
||||
export enum ANDROID_BUILD_TYPES {
|
||||
APK = 'apk',
|
||||
APP_BUNDLE = 'app-bundle',
|
||||
}
|
||||
|
||||
export { BUILD };
|
||||
export { IOS, BUILD };
|
||||
@@ -0,0 +1,5 @@
|
||||
export enum BUILD_TYPES {
|
||||
ARCHIVE = 'archive',
|
||||
CLIENT = 'client',
|
||||
SIMULATOR = 'simulator',
|
||||
}
|
||||
+2
-4
@@ -1,4 +1,4 @@
|
||||
import { ANDROID_BUILD_TYPES, IOS_BUILD_TYPES } from 'turtle/constants';
|
||||
import { IOS } from 'turtle/constants';
|
||||
|
||||
export interface IJob {
|
||||
platform: 'android' | 'ios';
|
||||
@@ -9,11 +9,9 @@ export interface IJob {
|
||||
config: {
|
||||
releaseChannel: string;
|
||||
publicUrl?: string;
|
||||
buildType: IOS_BUILD_TYPES & ANDROID_BUILD_TYPES;
|
||||
// ios
|
||||
buildType?: IOS.BUILD_TYPES;
|
||||
bundleIdentifier?: string;
|
||||
// android
|
||||
androidPackage?: string;
|
||||
};
|
||||
credentials: {
|
||||
// android
|
||||
|
||||
+35
-36
@@ -6,7 +6,6 @@ import BuildError from 'turtle/builders/BuildError';
|
||||
import { logErrorOnce } from 'turtle/builders/utils/common';
|
||||
import config from 'turtle/config';
|
||||
import { BUILD } from 'turtle/constants/index';
|
||||
import logger from 'turtle/logger';
|
||||
import * as buildDurationMetric from 'turtle/metrics/buildDuration';
|
||||
import * as buildStatusMetric from 'turtle/metrics/buildStatus';
|
||||
import { checkShouldExit, setCurrentJobId, turtleVersion } from 'turtle/turtleContext';
|
||||
@@ -14,35 +13,35 @@ import { getPriorities } from 'turtle/utils/priorities';
|
||||
import * as redis from 'turtle/utils/redis';
|
||||
import { sanitizeJob } from 'turtle/validator';
|
||||
|
||||
function _maybeExit() {
|
||||
function _maybeExit(logger: any) {
|
||||
if (checkShouldExit()) {
|
||||
logger.warn('Exiting due to previously received termination signal.');
|
||||
process.exit();
|
||||
}
|
||||
}
|
||||
|
||||
export async function doJob() {
|
||||
const jobData = await getJob();
|
||||
await processJob(jobData);
|
||||
_maybeExit();
|
||||
export async function doJob(logger: any) {
|
||||
const jobData = await getJob(logger);
|
||||
await processJob(jobData, logger);
|
||||
_maybeExit(logger);
|
||||
}
|
||||
|
||||
export async function getJob() {
|
||||
_maybeExit();
|
||||
export async function getJob(logger: any) {
|
||||
_maybeExit(logger);
|
||||
logger.info('Fetching job');
|
||||
while (true) {
|
||||
try {
|
||||
let job = null;
|
||||
const priorities = await getPriorities();
|
||||
const priorities = await getPriorities(logger);
|
||||
for (const priority of priorities) {
|
||||
job = await sqs.receiveMessage(priority);
|
||||
_maybeExit();
|
||||
job = await sqs.receiveMessage(priority, logger);
|
||||
_maybeExit(logger);
|
||||
if (job != null) {
|
||||
job.priority = priority;
|
||||
break;
|
||||
}
|
||||
}
|
||||
_maybeExit();
|
||||
_maybeExit(logger);
|
||||
if (job !== null) {
|
||||
return job;
|
||||
}
|
||||
@@ -52,34 +51,34 @@ export async function getJob() {
|
||||
}
|
||||
}
|
||||
|
||||
async function processJob(jobData: any) {
|
||||
async function processJob(jobData: any, logger: any) {
|
||||
const receiptHandle = jobData.ReceiptHandle;
|
||||
const { priority } = jobData;
|
||||
let timeoutId;
|
||||
try {
|
||||
const rawJob = JSON.parse(jobData.Body);
|
||||
timeoutId = failAfterMaxJobTime(priority, receiptHandle, rawJob);
|
||||
timeoutId = failAfterMaxJobTime(priority, receiptHandle, rawJob, logger);
|
||||
const job = await sanitizeJob(rawJob);
|
||||
|
||||
setCurrentJobId(job.id);
|
||||
const pingerHandle = sqs.changeMessageVisibilityRecurring(priority, jobData.ReceiptHandle, job.id);
|
||||
const pingerHandle = sqs.changeMessageVisibilityRecurring(priority, jobData.ReceiptHandle, job.id, logger);
|
||||
|
||||
logger.info(`Doing job MessageId=${jobData.MessageId} BuildId=${job.id} ${Date.now()}`);
|
||||
|
||||
const cancelled = await redis.checkIfCancelled(job.id);
|
||||
const cancelled = await redis.checkIfCancelled(job.id, logger);
|
||||
if (cancelled) {
|
||||
logger.info('The job has been cancelled');
|
||||
} else {
|
||||
redis.registerListener(job.id, () => deleteMessage(priority, receiptHandle));
|
||||
redis.registerListener(job.id, () => deleteMessage(priority, receiptHandle, logger), logger);
|
||||
const startTimestamp = Date.now();
|
||||
let buildFailed = false;
|
||||
const buildType = _.get(job, 'config.buildType', 'default');
|
||||
try {
|
||||
const status = await build(job);
|
||||
buildStatusMetric.add(buildType, priority, status);
|
||||
const status = await build(job, logger);
|
||||
buildStatusMetric.add(buildType, status);
|
||||
buildFailed = !status;
|
||||
} catch (err) {
|
||||
buildStatusMetric.add(buildType, priority, false);
|
||||
buildStatusMetric.add(buildType, false);
|
||||
buildFailed = true;
|
||||
throw err;
|
||||
} finally {
|
||||
@@ -93,13 +92,13 @@ async function processJob(jobData: any) {
|
||||
);
|
||||
buildDurationMetric.addTotalDuration(buildType, totalBuildDurationSecs, !buildFailed);
|
||||
}
|
||||
redis.unregisterListeners();
|
||||
redis.unregisterListeners(logger);
|
||||
}
|
||||
logger.info(`Job done MessageId=${jobData.MessageId} BuildId=${job.id} ${Date.now()}`);
|
||||
}
|
||||
clearInterval(pingerHandle);
|
||||
} finally {
|
||||
await deleteMessage(priority, receiptHandle);
|
||||
await deleteMessage(priority, receiptHandle, logger);
|
||||
if (timeoutId) {
|
||||
logger.info('Clearing job failer timeout...');
|
||||
clearTimeout(timeoutId);
|
||||
@@ -107,39 +106,39 @@ async function processJob(jobData: any) {
|
||||
}
|
||||
}
|
||||
|
||||
function failAfterMaxJobTime(priority: string, receiptHandle: string, job: any) {
|
||||
function failAfterMaxJobTime(priority: string, receiptHandle: string, job: any, logger: any) {
|
||||
return setTimeout(async () => {
|
||||
try {
|
||||
sqs.sendMessage(job.id, BUILD.JOB_STATES.ERRORED, { turtleVersion });
|
||||
await deleteMessage(priority, receiptHandle);
|
||||
sqs.sendMessage(job.id, BUILD.JOB_STATES.ERRORED, { turtleVersion }, logger);
|
||||
await deleteMessage(priority, receiptHandle, logger);
|
||||
} finally {
|
||||
logger.error('Build timed out. Going to terminate turtle agent.');
|
||||
await logger.cleanup();
|
||||
logger.error('Going to terminate turtle agent, just in case...');
|
||||
process.exit(1);
|
||||
}
|
||||
}, config.builder.maxJobTimeMs);
|
||||
}
|
||||
|
||||
async function build(job: any) {
|
||||
async function build(job: any, logger: any) {
|
||||
const startTimestamp = Date.now();
|
||||
const s3Url = await logger.initForJob(job);
|
||||
const s3Url = await logger.init(job);
|
||||
const buildLogger = logger.child({ jobID: job.id, experienceName: job.experienceName });
|
||||
const calculateBuildDuration = () => Math.ceil((Date.now() - startTimestamp) / 1000);
|
||||
|
||||
try {
|
||||
await sqs.sendMessage(job.id, BUILD.JOB_STATES.IN_PROGRESS, {
|
||||
logUrl: s3Url,
|
||||
logFormat: 'json',
|
||||
});
|
||||
const result = await (builders as any)[job.platform](job);
|
||||
}, buildLogger);
|
||||
const result = await builders[job.platform](job, buildLogger);
|
||||
const buildDuration = calculateBuildDuration();
|
||||
sqs.sendMessage(job.id, BUILD.JOB_STATES.FINISHED, {
|
||||
...result,
|
||||
turtleVersion,
|
||||
buildDuration,
|
||||
});
|
||||
}, buildLogger);
|
||||
return true;
|
||||
} catch (err) {
|
||||
logErrorOnce(err);
|
||||
logErrorOnce(err, buildLogger);
|
||||
const buildDuration = calculateBuildDuration();
|
||||
let reason;
|
||||
if (err instanceof BuildError) {
|
||||
@@ -149,14 +148,14 @@ async function build(job: any) {
|
||||
turtleVersion,
|
||||
buildDuration,
|
||||
...reason && { reason },
|
||||
});
|
||||
}, buildLogger);
|
||||
return false;
|
||||
} finally {
|
||||
await logger.cleanup();
|
||||
await logger.cleanup(job);
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteMessage(priority: string, receiptHandle: string) {
|
||||
async function deleteMessage(priority: string, receiptHandle: string, logger: any) {
|
||||
try {
|
||||
setCurrentJobId(null);
|
||||
await sqs.deleteMessage(priority, receiptHandle);
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import Joi from 'joi';
|
||||
|
||||
import { IOS_BUILD_TYPES, PLATFORMS } from 'turtle/constants/index';
|
||||
import { IOS, PLATFORMS } from 'turtle/constants/index';
|
||||
import baseJobSchema from 'turtle/jobsSchemas/base';
|
||||
|
||||
export default baseJobSchema.concat(
|
||||
Joi.object().keys({
|
||||
platform: Joi.string().valid(PLATFORMS.IOS),
|
||||
config: Joi.object().keys({
|
||||
buildType: Joi.string().default(IOS_BUILD_TYPES.ARCHIVE),
|
||||
buildType: Joi.string().default(IOS.BUILD_TYPES.ARCHIVE),
|
||||
bundleIdentifier: Joi.string().regex(/^[a-zA-Z][a-zA-Z0-9\-.]+$/),
|
||||
}),
|
||||
credentials: Joi.object()
|
||||
@@ -21,7 +21,7 @@ export default baseJobSchema.concat(
|
||||
udids: Joi.array().items(Joi.string()),
|
||||
})
|
||||
.when('config.buildType', {
|
||||
is: IOS_BUILD_TYPES.SIMULATOR,
|
||||
is: IOS.BUILD_TYPES.SIMULATOR,
|
||||
then: Joi.optional(),
|
||||
otherwise: Joi.required(),
|
||||
}),
|
||||
|
||||
+71
-64
@@ -1,75 +1,82 @@
|
||||
import bunyan from 'bunyan';
|
||||
import bunyanDebugStream from 'bunyan-debug-stream';
|
||||
import { SentryStream } from 'bunyan-sentry-stream';
|
||||
import { Client as RavenClient } from 'raven';
|
||||
|
||||
import config from 'turtle/config';
|
||||
import { IJob } from 'turtle/job';
|
||||
import streams from 'turtle/logger/streams';
|
||||
import S3Stream from 'turtle/logger/s3Stream';
|
||||
import { isOffline } from 'turtle/turtleContext';
|
||||
|
||||
interface ILoggerFields {
|
||||
[key: string]: string | number;
|
||||
// type error when using import
|
||||
// https://github.com/googleapis/nodejs-logging-bunyan/issues/241
|
||||
// tslint:disable-next-line:no-var-requires
|
||||
const { LoggingBunyan } = require('@google-cloud/logging-bunyan');
|
||||
|
||||
interface IStream {
|
||||
stream: any;
|
||||
type?: string;
|
||||
level?: string;
|
||||
reemitErrorEvents?: boolean;
|
||||
}
|
||||
|
||||
class Logger {
|
||||
private parentLogger: any;
|
||||
private currentLogger: any;
|
||||
export const s3logger = new S3Stream();
|
||||
|
||||
constructor(parentLogger?: any) {
|
||||
this.parentLogger = parentLogger || bunyan.createLogger({
|
||||
name: 'turtle',
|
||||
level: config.logger.level,
|
||||
platform: config.platform,
|
||||
...config.deploymentEnv && { environment: config.deploymentEnv },
|
||||
streams: Object.values(streams),
|
||||
});
|
||||
this.currentLogger = this.parentLogger;
|
||||
}
|
||||
const streams: IStream[] = [];
|
||||
|
||||
public async initForJob(job: IJob) {
|
||||
this.currentLogger = this.parentLogger.child({
|
||||
jobID: job.id,
|
||||
experienceName: job.experienceName,
|
||||
});
|
||||
const s3Url = await streams.s3.stream.init(job);
|
||||
return s3Url;
|
||||
}
|
||||
|
||||
public async cleanup() {
|
||||
this.currentLogger = this.parentLogger;
|
||||
await streams.s3.stream.cleanup();
|
||||
}
|
||||
|
||||
public trace(...all: any[]) {
|
||||
this.currentLogger.trace(...all);
|
||||
}
|
||||
|
||||
public debug(...all: any[]) {
|
||||
this.currentLogger.debug(...all);
|
||||
}
|
||||
|
||||
public info(...all: any[]) {
|
||||
this.currentLogger.info(...all);
|
||||
}
|
||||
|
||||
public warn(...all: any[]) {
|
||||
this.currentLogger.warn(...all);
|
||||
}
|
||||
|
||||
public error(...all: any[]) {
|
||||
this.currentLogger.error(...all);
|
||||
}
|
||||
|
||||
public fatal(...all: any[]) {
|
||||
this.currentLogger.fatal(...all);
|
||||
}
|
||||
|
||||
public child(fields: ILoggerFields) {
|
||||
const newLogger = this.currentLogger.child(fields);
|
||||
return new Logger(newLogger);
|
||||
}
|
||||
|
||||
// only for backward compatibility
|
||||
public withFields(fields: ILoggerFields) {
|
||||
return this.child(fields);
|
||||
}
|
||||
if (isOffline()) {
|
||||
const prettyStdOut = new bunyanDebugStream({ forceColor: true });
|
||||
streams.push({ stream: prettyStdOut, type: 'raw', level: config.logger.client.level });
|
||||
} else {
|
||||
streams.push(
|
||||
{ stream: process.stdout },
|
||||
{
|
||||
type: 'raw',
|
||||
stream: s3logger,
|
||||
reemitErrorEvents: true,
|
||||
level: config.logger.client.level,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
export default new Logger();
|
||||
if (config.sentry.dsn && config.deploymentEnv !== 'development') {
|
||||
const raven = new RavenClient();
|
||||
raven.config(config.sentry.dsn, { environment: config.deploymentEnv });
|
||||
streams.push({
|
||||
level: 'error',
|
||||
type: 'raw', // Mandatory type for SentryStream
|
||||
stream: new SentryStream(raven),
|
||||
});
|
||||
}
|
||||
|
||||
if (config.google.credentials) {
|
||||
const resource = {
|
||||
type: 'generic_node',
|
||||
labels: {
|
||||
node_id: config.hostname,
|
||||
location: '', // default value
|
||||
namespace: '', // default value
|
||||
},
|
||||
};
|
||||
streams.push(new LoggingBunyan({ name: 'turtle', resource }).stream('info'));
|
||||
}
|
||||
|
||||
const logger = bunyan.createLogger({
|
||||
name: 'turtle',
|
||||
level: config.logger.level,
|
||||
platform: config.platform,
|
||||
environment: config.deploymentEnv,
|
||||
streams,
|
||||
});
|
||||
|
||||
logger.init = async (job: IJob) => {
|
||||
return await s3logger.init(job);
|
||||
};
|
||||
|
||||
logger.cleanup = async () => {
|
||||
// a little hacky, but works
|
||||
logger.info({ lastBuildLog: true }, 'this is the last log from this build');
|
||||
await s3logger.waitForLogger();
|
||||
};
|
||||
|
||||
export default logger;
|
||||
@@ -0,0 +1,111 @@
|
||||
|
||||
import EventEmitter from 'events';
|
||||
import path from 'path';
|
||||
|
||||
import fs from 'fs-extra';
|
||||
import _ from 'lodash';
|
||||
|
||||
import { uploadFile } from 'turtle/aws/s3';
|
||||
import config from 'turtle/config';
|
||||
import * as constants from 'turtle/constants/logger';
|
||||
import { IJob } from 'turtle/job';
|
||||
|
||||
export default class S3Stream extends EventEmitter {
|
||||
private s3Url: string | null;
|
||||
private lastFlush: number | null;
|
||||
private filePath: string | null;
|
||||
private fileHandle: number | null;
|
||||
private finishedPromise: Promise<any> | null;
|
||||
private finishedPromiseResolveFn: any;
|
||||
private secrets: any;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.s3Url = null;
|
||||
this.lastFlush = null;
|
||||
this.filePath = null;
|
||||
this.fileHandle = null;
|
||||
this.finishedPromise = null;
|
||||
this.finishedPromiseResolveFn = null;
|
||||
}
|
||||
|
||||
public async init(job: IJob) {
|
||||
this.s3Url = `logs/${job.experienceName}/${job.id}`;
|
||||
const dir = config.directories.tempS3LogsDir;
|
||||
const exists = await fs.pathExists(dir);
|
||||
if (!exists) {
|
||||
await fs.mkdir(dir);
|
||||
}
|
||||
this.filePath = path.join(dir, job.id);
|
||||
this.fileHandle = await fs.open(this.filePath, 'w+', 0o660);
|
||||
this.secrets = {};
|
||||
this.finishedPromise = new Promise((resolveFn) => (this.finishedPromiseResolveFn = resolveFn));
|
||||
const res = await this.flush() as any;
|
||||
return res.Location;
|
||||
}
|
||||
|
||||
public async cleanup() {
|
||||
const res = this.finishedPromiseResolveFn;
|
||||
try {
|
||||
await this.flush(true);
|
||||
await fs.close(this.fileHandle as number);
|
||||
this.fileHandle = null;
|
||||
this.lastFlush = null;
|
||||
this.secrets = {};
|
||||
} finally {
|
||||
this.finishedPromise = null;
|
||||
this.finishedPromiseResolveFn = null;
|
||||
res();
|
||||
}
|
||||
}
|
||||
|
||||
public flush(force = false) {
|
||||
if (force || !this.lastFlush) {
|
||||
return this._flush();
|
||||
}
|
||||
const now = Date.now();
|
||||
if (now - this.lastFlush > config.logger.interval) {
|
||||
return this._flush();
|
||||
}
|
||||
}
|
||||
|
||||
public write(recRaw: any) {
|
||||
if (this.fileHandle) {
|
||||
const rec = JSON.stringify({
|
||||
...recRaw,
|
||||
level: constants.LEVELS_MAPPING[recRaw.level].toLowerCase(),
|
||||
});
|
||||
if (recRaw.lastBuildLog) {
|
||||
return this.cleanup();
|
||||
}
|
||||
const recSanitized = Object.keys(this.secrets).reduce(
|
||||
(acc, secret) => acc.replace(new RegExp(secret, 'g'), this.secrets[secret]),
|
||||
rec,
|
||||
);
|
||||
fs
|
||||
.write(this.fileHandle as number, `${recSanitized}\n`)
|
||||
.then(() => {
|
||||
this.flush();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public addSecretsToFilter(secrets: any) {
|
||||
const filteredSecrets = secrets.filter((i: any) => i !== undefined && i !== null);
|
||||
const replacements = filteredSecrets.map((i: any) => 'X'.repeat(i.length));
|
||||
const toAdd = _.zipObject(filteredSecrets, replacements);
|
||||
this.secrets = Object.assign({}, this.secrets, toAdd);
|
||||
}
|
||||
|
||||
public waitForLogger() {
|
||||
return this.finishedPromise;
|
||||
}
|
||||
|
||||
private _flush() {
|
||||
this.lastFlush = Date.now();
|
||||
return uploadFile({
|
||||
srcPath: this.filePath as string,
|
||||
key: this.s3Url as string,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
import config from 'turtle/config';
|
||||
|
||||
// type error when using import
|
||||
// https://github.com/googleapis/nodejs-logging-bunyan/issues/241
|
||||
// tslint:disable-next-line:no-var-requires
|
||||
const { LoggingBunyan } = require('@google-cloud/logging-bunyan');
|
||||
|
||||
export default function create() {
|
||||
if (!config.google.credentials) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const gcloudStream = new LoggingBunyan({
|
||||
name: 'turtle',
|
||||
resource: {
|
||||
type: 'generic_node',
|
||||
labels: {
|
||||
node_id: config.hostname,
|
||||
location: '', // default value
|
||||
namespace: '', // default value
|
||||
},
|
||||
},
|
||||
});
|
||||
return {
|
||||
level: config.logger.level,
|
||||
type: 'raw',
|
||||
stream: gcloudStream,
|
||||
};
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
import pickBy from 'lodash/pickBy';
|
||||
|
||||
import createGCloudStream from 'turtle/logger/streams/gcloud';
|
||||
import createOfflineStream from 'turtle/logger/streams/offline';
|
||||
import createS3Stream from 'turtle/logger/streams/s3';
|
||||
import createSentryStream from 'turtle/logger/streams/sentry';
|
||||
import { isOffline } from 'turtle/turtleContext';
|
||||
|
||||
interface IStream {
|
||||
stream: any;
|
||||
type?: string;
|
||||
level?: string;
|
||||
reemitErrorEvents?: boolean;
|
||||
}
|
||||
|
||||
interface IStreamMap {
|
||||
[key: string]: IStream;
|
||||
}
|
||||
|
||||
function prepareStreams(): IStreamMap {
|
||||
if (isOffline()) {
|
||||
return {
|
||||
stdout: createOfflineStream(),
|
||||
};
|
||||
} else {
|
||||
const allStreams = {
|
||||
stdout: { stream: process.stdout },
|
||||
gcloud: createGCloudStream(),
|
||||
s3: createS3Stream(),
|
||||
sentry: createSentryStream(),
|
||||
};
|
||||
return pickBy(allStreams) as IStreamMap;
|
||||
}
|
||||
}
|
||||
|
||||
const streams = prepareStreams();
|
||||
|
||||
export default streams;
|
||||
@@ -1,11 +0,0 @@
|
||||
import bunyanDebugStream from 'bunyan-debug-stream';
|
||||
import config from 'turtle/config';
|
||||
|
||||
export default function create() {
|
||||
const prettyStdOut = new bunyanDebugStream({ forceColor: true });
|
||||
return {
|
||||
stream: prettyStdOut,
|
||||
type: 'raw',
|
||||
level: config.logger.level,
|
||||
};
|
||||
}
|
||||
@@ -1,106 +0,0 @@
|
||||
|
||||
import EventEmitter from 'events';
|
||||
import path from 'path';
|
||||
|
||||
import fs from 'fs-extra';
|
||||
import _ from 'lodash';
|
||||
|
||||
import { uploadFile } from 'turtle/aws/s3';
|
||||
import config from 'turtle/config';
|
||||
import { IJob } from 'turtle/job';
|
||||
|
||||
export default function create() {
|
||||
const s3 = new S3Stream();
|
||||
return {
|
||||
type: 'raw',
|
||||
stream: s3,
|
||||
reemitErrorEvents: true,
|
||||
level: config.logger.level,
|
||||
};
|
||||
}
|
||||
|
||||
class S3Stream extends EventEmitter {
|
||||
private s3Url: string | null = null;
|
||||
private lastFlush: number | null = null;
|
||||
private filePath: string | null = null;
|
||||
private fileHandle: number | null = null;
|
||||
private uploadingPromise: Promise<void> | null = null;
|
||||
private uploadingPromiseResolveFn: any;
|
||||
private waitingOnPromise: boolean = false;
|
||||
|
||||
public async init(job: IJob) {
|
||||
this.s3Url = `logs/${job.experienceName}/${job.id}`;
|
||||
const dir = config.directories.tempS3LogsDir;
|
||||
const exists = await fs.pathExists(dir);
|
||||
if (!exists) {
|
||||
await fs.mkdir(dir);
|
||||
}
|
||||
this.filePath = path.join(dir, job.id);
|
||||
this.fileHandle = await fs.open(this.filePath, 'w+', 0o660);
|
||||
this.lastFlush = null;
|
||||
|
||||
const res = await this.flush() as any;
|
||||
return res.Location;
|
||||
}
|
||||
|
||||
public async cleanup() {
|
||||
await this.flush(true);
|
||||
await fs.close(this.fileHandle as number);
|
||||
this.fileHandle = null;
|
||||
this.waitingOnPromise = false;
|
||||
}
|
||||
|
||||
public flush(force = false) {
|
||||
if (force || !this.lastFlush) {
|
||||
return this.flushInternal();
|
||||
}
|
||||
const now = Date.now();
|
||||
if (now - this.lastFlush > config.logger.intervalMs) {
|
||||
return this.flushInternal();
|
||||
}
|
||||
}
|
||||
|
||||
public write(rec: any) {
|
||||
if (!this.fileHandle) {
|
||||
return;
|
||||
}
|
||||
fs
|
||||
.write(this.fileHandle as number, `${JSON.stringify(rec)}\n`)
|
||||
.then(() => {
|
||||
this.flush();
|
||||
});
|
||||
}
|
||||
|
||||
private flushInternal(): any {
|
||||
this.lastFlush = Date.now();
|
||||
if (!this.uploadingPromise) {
|
||||
this.uploadingPromise = new Promise((res) => {
|
||||
this.uploadingPromiseResolveFn = res;
|
||||
});
|
||||
return this
|
||||
.upload()
|
||||
.then((result) => {
|
||||
setTimeout(() => this.uploadingPromiseResolveFn(), 0);
|
||||
return result;
|
||||
});
|
||||
} else {
|
||||
if (this.waitingOnPromise) {
|
||||
return;
|
||||
}
|
||||
this.waitingOnPromise = true;
|
||||
return this.uploadingPromise
|
||||
.then(() => {
|
||||
this.waitingOnPromise = false;
|
||||
this.uploadingPromise = null;
|
||||
return this.flushInternal();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private upload() {
|
||||
return uploadFile({
|
||||
srcPath: this.filePath as string,
|
||||
key: this.s3Url as string,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
import { SentryStream } from 'bunyan-sentry-stream';
|
||||
import { Client as RavenClient } from 'raven';
|
||||
|
||||
import config from 'turtle/config';
|
||||
|
||||
export default function create() {
|
||||
if (!config.sentry.dsn || config.deploymentEnv === 'development') {
|
||||
return null;
|
||||
}
|
||||
|
||||
const raven = new RavenClient();
|
||||
raven.config(config.sentry.dsn, { environment: config.deploymentEnv });
|
||||
return {
|
||||
level: 'error',
|
||||
type: 'raw', // Mandatory type for SentryStream
|
||||
stream: new SentryStream(raven),
|
||||
};
|
||||
}
|
||||
@@ -1,11 +1,10 @@
|
||||
import * as cloudwatch from 'turtle/aws/cloudwatch';
|
||||
import { TYPE_DIMENSIONS } from 'turtle/aws/cloudwatch/dimensions';
|
||||
import logger from 'turtle/logger';
|
||||
|
||||
const TURTLE_BUILD_DURATION_METRIC_NAME = 'turtleBuildDuration';
|
||||
const TOTAL_BUILD_DURATION_METRIC_NAME = 'totalBuildDuration';
|
||||
|
||||
export function register() {
|
||||
export function register(logger: any) {
|
||||
for (const metricName of [TURTLE_BUILD_DURATION_METRIC_NAME, TOTAL_BUILD_DURATION_METRIC_NAME]) {
|
||||
logger.info(`Registered AWS Cloudwatch metric: ${metricName}`);
|
||||
cloudwatch.registerMetric({
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
import * as cloudwatch from 'turtle/aws/cloudwatch';
|
||||
import { PRIORITY_DIMENSIONS } from 'turtle/aws/cloudwatch/dimensions';
|
||||
import { TYPE_DIMENSIONS } from 'turtle/aws/cloudwatch/dimensions';
|
||||
import * as reducers from 'turtle/aws/cloudwatch/reducers';
|
||||
import config from 'turtle/config';
|
||||
import logger from 'turtle/logger';
|
||||
|
||||
const BUILD_METRIC_NAME = 'build';
|
||||
|
||||
export function register() {
|
||||
export function register(logger: any) {
|
||||
logger.info(`Registered AWS Cloudwatch metric: ${BUILD_METRIC_NAME}`);
|
||||
cloudwatch.registerMetric({
|
||||
metricName: BUILD_METRIC_NAME,
|
||||
@@ -24,11 +23,11 @@ export function register() {
|
||||
});
|
||||
}
|
||||
|
||||
export function add(type: string, priority: string, success = true) {
|
||||
export function add(type: string, success = true) {
|
||||
cloudwatch.addMetricData({
|
||||
name: BUILD_METRIC_NAME,
|
||||
value: 1,
|
||||
success,
|
||||
extraDimensions: PRIORITY_DIMENSIONS(type, priority),
|
||||
extraDimensions: TYPE_DIMENSIONS(type),
|
||||
});
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
import groupBy from 'lodash/groupBy';
|
||||
import mapValues from 'lodash/mapValues';
|
||||
import take from 'lodash/take';
|
||||
|
||||
import logger from 'turtle/logger';
|
||||
import { sendMetric } from 'turtle/metrics/datadog';
|
||||
import { getReplicaCount } from 'turtle/utils/metadata';
|
||||
import { getLabeledConfiguration, TurtleModeLabels } from 'turtle/utils/priorities';
|
||||
|
||||
const createMetricName = () => `turtle.replicaCount`;
|
||||
const createModeMetricName = (mode: TurtleModeLabels) => `${createMetricName()}.${mode}`;
|
||||
const createModeTag = (mode: TurtleModeLabels) => `mode:${mode}`;
|
||||
|
||||
export default async function sendConfigToDatadog() {
|
||||
const configuration = await getLabeledConfiguration();
|
||||
const replicaCount = await getReplicaCount();
|
||||
const currentReplicaCount = replicaCount === null ? configuration.length : replicaCount;
|
||||
|
||||
const replicaCounts = mapValues(
|
||||
groupBy<string>(take(configuration, currentReplicaCount)),
|
||||
(i) => i.length,
|
||||
);
|
||||
|
||||
const metricName = createMetricName();
|
||||
|
||||
const normalReplicaCount = replicaCounts[TurtleModeLabels.Normal] || 0;
|
||||
const highReplicaCount = replicaCounts[TurtleModeLabels.High] || 0;
|
||||
const highOnlyReplicaCount = replicaCounts[TurtleModeLabels.HighOnly] || 0;
|
||||
|
||||
const normalTag = createModeTag(TurtleModeLabels.Normal);
|
||||
const normalMetricName = createModeMetricName(TurtleModeLabels.Normal);
|
||||
logger.debug(
|
||||
`Sending ${metricName}=${normalReplicaCount} (${normalTag}), ${normalMetricName}=${normalReplicaCount} to Datadog`,
|
||||
);
|
||||
await sendMetric(metricName, normalReplicaCount, [normalTag]);
|
||||
await sendMetric(normalMetricName, normalReplicaCount);
|
||||
|
||||
const highTag = createModeTag(TurtleModeLabels.High);
|
||||
const highMetricName = createModeMetricName(TurtleModeLabels.High);
|
||||
logger.debug(
|
||||
`Sending ${metricName}=${highReplicaCount} (${highTag}), ${highMetricName}=${highReplicaCount} to Datadog`,
|
||||
);
|
||||
await sendMetric(metricName, highReplicaCount, [highTag]);
|
||||
await sendMetric(highMetricName, highReplicaCount);
|
||||
|
||||
const highOnlyTag = createModeTag(TurtleModeLabels.HighOnly);
|
||||
const highOnlyMetricName = createModeMetricName(TurtleModeLabels.HighOnly);
|
||||
logger.debug(
|
||||
`Sending ${metricName}=${highOnlyReplicaCount} (${highOnlyTag}),`
|
||||
+ ` ${highOnlyMetricName}=${highOnlyReplicaCount} to Datadog`,
|
||||
);
|
||||
await sendMetric(metricName, highOnlyReplicaCount, [highOnlyTag]);
|
||||
await sendMetric(highOnlyMetricName, highOnlyReplicaCount);
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
import dogapi from 'dogapi';
|
||||
import util from 'util';
|
||||
|
||||
import config from 'turtle/config';
|
||||
|
||||
const datadogSendMetric = util.promisify(dogapi.metric.send);
|
||||
|
||||
if (!config.datadog.disabled) {
|
||||
dogapi.initialize({
|
||||
api_key: config.datadog.apiKey,
|
||||
app_key: config.datadog.appKey,
|
||||
});
|
||||
}
|
||||
|
||||
const tags = [
|
||||
`env:${config.deploymentEnv}`,
|
||||
`platform:${config.platform}`,
|
||||
];
|
||||
|
||||
export function sendMetric(name: string, value: number, additionalTags?: string[]) {
|
||||
if (!config.datadog.disabled) {
|
||||
return datadogSendMetric(name, value, { tags: [...tags, ...(additionalTags || [])] });
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,11 @@
|
||||
import * as cloudwatch from 'turtle/aws/cloudwatch';
|
||||
import logger from 'turtle/logger';
|
||||
import * as buildDuration from 'turtle/metrics/buildDuration';
|
||||
import * as buildStatus from 'turtle/metrics/buildStatus';
|
||||
|
||||
const metricsToRegister = [buildStatus, buildDuration];
|
||||
|
||||
export async function init() {
|
||||
export async function init(logger: any) {
|
||||
logger.info('Initializing Cloudwatch metrics');
|
||||
await cloudwatch.init();
|
||||
metricsToRegister.forEach((metric) => metric.register());
|
||||
await cloudwatch.init(logger);
|
||||
metricsToRegister.forEach((metric) => metric.register(logger));
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
import isNaN from 'lodash/isNaN';
|
||||
|
||||
import { PLATFORMS } from 'turtle/constants';
|
||||
import logger from 'turtle/logger';
|
||||
import { setReplicaCount } from 'turtle/utils/metadata';
|
||||
import { getRedisClient, RedisClient } from 'turtle/utils/redis';
|
||||
|
||||
async function run() {
|
||||
const platform = process.env.SCRIPT_PLATFORM;
|
||||
const replicaCountRaw = process.env.SCRIPT_REPLICA_COUNT;
|
||||
const replicaCount = Number(replicaCountRaw);
|
||||
|
||||
if (!platform || !replicaCountRaw) {
|
||||
throw new Error('You must set both SCRIPT_PLATFORM and SCRIPT_REPLICA_COUNT env variables.');
|
||||
}
|
||||
|
||||
if (!Object.values(PLATFORMS).includes(platform)) {
|
||||
throw new Error(`Unsupported platform: ${platform}`);
|
||||
}
|
||||
|
||||
if (isNaN(replicaCount)) {
|
||||
throw new Error(`Wrong value for replica count: ${replicaCountRaw}`);
|
||||
}
|
||||
|
||||
const redis = await getRedisClient(RedisClient.Configuration);
|
||||
logger.info(`[${platform}] Setting replica count in redis to ${replicaCount}`);
|
||||
await setReplicaCount(platform as PLATFORMS, replicaCount);
|
||||
redis.disconnect();
|
||||
}
|
||||
|
||||
if (require.main === module) {
|
||||
run()
|
||||
.then(() => logger.info('All done'))
|
||||
.catch((err) => {
|
||||
logger.error('Error:', err);
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
+4
-4
@@ -1,4 +1,4 @@
|
||||
import { LoggerDetach } from '@expo/xdl';
|
||||
import { LoggerDetach } from 'xdl';
|
||||
|
||||
import config from 'turtle/config';
|
||||
import { doJob } from 'turtle/jobManager';
|
||||
@@ -31,11 +31,11 @@ async function main() {
|
||||
);
|
||||
LoggerDetach.configure(logger);
|
||||
if (setup[config.platform]) {
|
||||
await setup[config.platform]();
|
||||
await setup[config.platform](logger);
|
||||
}
|
||||
|
||||
try {
|
||||
const redis = await getRedisClient(RedisClient.Configuration);
|
||||
const redis = await getRedisClient(logger, RedisClient.Configuration);
|
||||
await redis.set(REDIS_TURTLE_VERSION_KEY, turtleVersion);
|
||||
logger.info(`Register Turtle version ${turtleVersion} in Redis`);
|
||||
} catch (err) {
|
||||
@@ -44,7 +44,7 @@ async function main() {
|
||||
|
||||
while (true) {
|
||||
try {
|
||||
await doJob();
|
||||
await doJob(logger);
|
||||
} catch (err) {
|
||||
logger.error({ err }, 'Failed to do a job');
|
||||
}
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import logger from 'turtle/logger';
|
||||
import commonSetup from 'turtle/setup/common';
|
||||
|
||||
export default async function setup() {
|
||||
export default async function setup(logger: any) {
|
||||
logger.info('Setting up environment...');
|
||||
await commonSetup();
|
||||
await commonSetup(logger);
|
||||
logger.info('Finished setting up environment');
|
||||
}
|
||||
+2
-15
@@ -1,23 +1,10 @@
|
||||
import fs from 'fs-extra';
|
||||
|
||||
import config from 'turtle/config';
|
||||
import logger from 'turtle/logger';
|
||||
import * as metrics from 'turtle/metrics';
|
||||
import sendConfigToDatadog from 'turtle/metrics/buildersConfig';
|
||||
|
||||
export default async function commonSetup() {
|
||||
announceBuildersConfig();
|
||||
await metrics.init();
|
||||
export default async function commonSetup(logger: any) {
|
||||
await metrics.init(logger);
|
||||
await fs.remove(config.directories.temporaryFilesRoot);
|
||||
await fs.remove(config.directories.tempS3LogsDir);
|
||||
}
|
||||
|
||||
async function announceBuildersConfig() {
|
||||
try {
|
||||
await sendConfigToDatadog();
|
||||
} catch (err) {
|
||||
logger.error({ err }, 'Failed to send metrics to Datadog');
|
||||
} finally {
|
||||
setTimeout(announceBuildersConfig, config.datadog.intervalMs);
|
||||
}
|
||||
}
|
||||
+2
-3
@@ -3,7 +3,6 @@ import os from 'os';
|
||||
import path from 'path';
|
||||
|
||||
import * as keychain from 'turtle/builders/utils/ios/keychain';
|
||||
import logger from 'turtle/logger';
|
||||
import commonSetup from 'turtle/setup/common';
|
||||
|
||||
async function deleteProvisioningProfilesFromHomedir() {
|
||||
@@ -17,10 +16,10 @@ async function deleteProvisioningProfilesFromHomedir() {
|
||||
}
|
||||
}
|
||||
|
||||
export default async function setup() {
|
||||
export default async function setup(logger: any) {
|
||||
logger.info('Setting up environment...');
|
||||
await deleteProvisioningProfilesFromHomedir();
|
||||
await keychain.cleanUp();
|
||||
await commonSetup();
|
||||
await commonSetup(logger);
|
||||
logger.info('Finished setting up environment');
|
||||
}
|
||||
Vendored
+4
-5
@@ -1,10 +1,9 @@
|
||||
declare module 'async-retry';
|
||||
declare module 'copy';
|
||||
declare module 'xdl';
|
||||
declare module 'bunyan';
|
||||
declare module 'bunyan-sentry-stream';
|
||||
declare module 'bunyan-debug-stream';
|
||||
declare module 'copy';
|
||||
declare module 'dogapi';
|
||||
declare module 'async-retry';
|
||||
declare module 'ioredis';
|
||||
declare module 'is-semver';
|
||||
declare module 'request-progress';
|
||||
declare module '@expo/xdl';
|
||||
declare module 'is-semver';
|
||||
@@ -16,16 +16,5 @@ export const env = (varName: string, defaultValue?: any) => {
|
||||
export const envOptional = (varName: string) => env(varName, null);
|
||||
export const envTransform = (varName: string, defaultValue: any, fn: any) => fn(env(varName, defaultValue));
|
||||
export const envNum = (varName: string, defaultValue?: number) => parseInt(env(varName, defaultValue), 10);
|
||||
export const envBoolean = (varName: string, defaultValue?: boolean) => {
|
||||
let defaultString;
|
||||
if (defaultValue === true) {
|
||||
defaultString = 'true';
|
||||
} else if (defaultValue === false) {
|
||||
defaultString = 'false';
|
||||
} else {
|
||||
defaultString = defaultValue;
|
||||
}
|
||||
return env(varName, defaultString) === 'true';
|
||||
};
|
||||
|
||||
const isOffline = () => process.env.TURTLE_MODE === 'offline';
|
||||
@@ -1,21 +0,0 @@
|
||||
import config from 'turtle/config';
|
||||
import { PLATFORMS } from 'turtle/constants';
|
||||
import { getRedisClient, RedisClient } from 'turtle/utils/redis';
|
||||
|
||||
const createKeyPrefix = (platform: PLATFORMS) => `${config.deploymentEnv}:${platform}`;
|
||||
const createReplicaCountKey = (platform: PLATFORMS) => `${createKeyPrefix(platform)}:replicaCount`;
|
||||
|
||||
export async function setReplicaCount(platform: PLATFORMS, replicaCount: number) {
|
||||
const redis = await getRedisClient(RedisClient.Configuration);
|
||||
await redis.set(
|
||||
createReplicaCountKey(platform),
|
||||
replicaCount,
|
||||
);
|
||||
}
|
||||
|
||||
export async function getReplicaCount(maybePlatform?: PLATFORMS) {
|
||||
const platform = maybePlatform || config.platform;
|
||||
const redis = await getRedisClient(RedisClient.Configuration);
|
||||
const result = await redis.get(createReplicaCountKey(platform));
|
||||
return result === null ? null : Number(result);
|
||||
}
|
||||
+2
-29
@@ -1,23 +1,14 @@
|
||||
import Joi from 'joi';
|
||||
import isEqual from 'lodash/isEqual';
|
||||
import os from 'os';
|
||||
|
||||
import config from 'turtle/config';
|
||||
import logger from 'turtle/logger';
|
||||
import { getRedisClient, RedisClient } from 'turtle/utils/redis';
|
||||
|
||||
export const NORMAL = 'normalPriority';
|
||||
export const HIGH = 'highPriority';
|
||||
|
||||
export enum TurtleModeLabels {
|
||||
Normal = 'normal',
|
||||
High = 'high',
|
||||
HighOnly = 'highOnly',
|
||||
}
|
||||
|
||||
export const NORMAL_CONFIGURATION = [NORMAL, HIGH];
|
||||
export const HIGH_CONFIGURATION = [HIGH, NORMAL];
|
||||
export const HIGH_ONLY_CONFIGURATION = [HIGH];
|
||||
|
||||
const REDIS_GET_CONFIG = `
|
||||
local i = 0
|
||||
@@ -48,7 +39,7 @@ export function createDefaultConfigurationKey(platform = config.platform) {
|
||||
return `${configPrefix(platform)}:default`;
|
||||
}
|
||||
|
||||
export async function getPriorities() {
|
||||
export async function getPriorities(logger: any) {
|
||||
if (config.env === 'test') {
|
||||
return NORMAL_CONFIGURATION;
|
||||
}
|
||||
@@ -63,7 +54,7 @@ export async function getPriorities() {
|
||||
}
|
||||
|
||||
try {
|
||||
const configurations = await getConfiguration(redis);
|
||||
const configurations = JSON.parse(await redis.get(createConfigurationsKey()));
|
||||
const configurationsIndex = await redis.getConfig(createConfigurationsKey(), os.hostname());
|
||||
logger.debug(`Using configuration at index ${configurationsIndex} pulled from redis`);
|
||||
const { error, value: configValue } = configSchema.validate(configurations[configurationsIndex]);
|
||||
@@ -92,21 +83,3 @@ export async function getPriorities() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function getConfiguration(currentRedisClient: any) {
|
||||
const redis = currentRedisClient || await getRedisClient(RedisClient.Configuration);
|
||||
return JSON.parse(await redis.get(createConfigurationsKey()));
|
||||
}
|
||||
|
||||
export async function getLabeledConfiguration(currentRedisClient?: any) {
|
||||
const configuration = await getConfiguration(currentRedisClient);
|
||||
return configuration.map((i: string[]) => {
|
||||
if (isEqual(i, NORMAL_CONFIGURATION)) {
|
||||
return TurtleModeLabels.Normal;
|
||||
} else if (isEqual(i, HIGH_CONFIGURATION)) {
|
||||
return TurtleModeLabels.High;
|
||||
} else {
|
||||
return TurtleModeLabels.HighOnly;
|
||||
}
|
||||
});
|
||||
}
|
||||
+7
-9
@@ -1,7 +1,6 @@
|
||||
import Redis from 'ioredis';
|
||||
|
||||
import config from 'turtle/config';
|
||||
import logger from 'turtle/logger';
|
||||
|
||||
const MILLIS_TO_UPLOAD_LOGS = 3000;
|
||||
export const MILLIS_CONNECTION_TIMEOUT = 10000;
|
||||
@@ -20,7 +19,7 @@ const clientToUrl: IClientConfig = {
|
||||
[RedisClient.Configuration]: config.redis.configUrl,
|
||||
};
|
||||
|
||||
export function connect(timeoutMs: number, type: string): Promise<any> {
|
||||
export function connect(timeoutMs: number, type: string) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const redisClient = new Redis(clientToUrl[type], {
|
||||
maxRetriesPerRequest: 2,
|
||||
@@ -47,7 +46,7 @@ const redisClients: IRedisClients = {
|
||||
[RedisClient.Configuration]: null,
|
||||
};
|
||||
|
||||
export async function getRedisClient(type = RedisClient.Default) {
|
||||
export async function getRedisClient(logger: any, type = RedisClient.Default) {
|
||||
if (!redisClients[type]) {
|
||||
try {
|
||||
redisClients[type] = await connect(MILLIS_CONNECTION_TIMEOUT, type);
|
||||
@@ -58,9 +57,9 @@ export async function getRedisClient(type = RedisClient.Default) {
|
||||
return redisClients[type];
|
||||
}
|
||||
|
||||
export async function checkIfCancelled(jobId: string) {
|
||||
export async function checkIfCancelled(jobId: string, logger: any) {
|
||||
try {
|
||||
const redis = await getRedisClient();
|
||||
const redis = await getRedisClient(logger);
|
||||
return await redis.get(`jobs:cancelled:${jobId}`);
|
||||
} catch (err) {
|
||||
if (config.deploymentEnv === 'development') {
|
||||
@@ -71,7 +70,7 @@ export async function checkIfCancelled(jobId: string) {
|
||||
}
|
||||
}
|
||||
|
||||
export async function registerListener(jobId: string, deleteMessage: any) {
|
||||
export async function registerListener(jobId: string, deleteMessage: any, logger: any) {
|
||||
try {
|
||||
const redis = await getRedisClient(RedisClient.Subscriber);
|
||||
redis.subscribe('jobs:cancelled');
|
||||
@@ -79,7 +78,6 @@ export async function registerListener(jobId: string, deleteMessage: any) {
|
||||
if (message === jobId) {
|
||||
logger.info({ lastBuildLog: true }, 'Job cancelled - killing process');
|
||||
await deleteMessage();
|
||||
await logger.cleanup();
|
||||
setTimeout(() => process.exit(1), MILLIS_TO_UPLOAD_LOGS);
|
||||
}
|
||||
});
|
||||
@@ -89,9 +87,9 @@ export async function registerListener(jobId: string, deleteMessage: any) {
|
||||
}
|
||||
}
|
||||
|
||||
export async function unregisterListeners() {
|
||||
export async function unregisterListeners(logger: any) {
|
||||
try {
|
||||
const redis = await getRedisClient();
|
||||
const redis = await getRedisClient(logger);
|
||||
redis.removeAllListeners('message');
|
||||
} catch (err) {
|
||||
logger.error(err);
|
||||
|
||||
Executable
+3
@@ -0,0 +1,3 @@
|
||||
#! /usr/bin/env bash
|
||||
# This file was generated by nix-binstubs
|
||||
exec nix run -f "$(dirname "$0")/../../nix" nodejs-8_x --command node "$@"
|
||||
Executable
+3
@@ -0,0 +1,3 @@
|
||||
#! /usr/bin/env bash
|
||||
# This file was generated by nix-binstubs
|
||||
exec nix run -f "$(dirname "$0")/../../nix" nodejs-8_x --command npm "$@"
|
||||
Executable
+3
@@ -0,0 +1,3 @@
|
||||
#! /usr/bin/env bash
|
||||
# This file was generated by nix-binstubs
|
||||
exec nix run -f "$(dirname "$0")/../../nix" nodejs-8_x --command npx "$@"
|
||||
Executable
+3
@@ -0,0 +1,3 @@
|
||||
#! /usr/bin/env bash
|
||||
# This file was generated by nix-binstubs
|
||||
exec nix run -f "$(dirname "$0")/../../nix" yarn --command yarn "$@"
|
||||
Executable
+3
@@ -0,0 +1,3 @@
|
||||
#! /usr/bin/env bash
|
||||
# This file was generated by nix-binstubs
|
||||
exec nix run -f "$(dirname "$0")/../../nix" yarn --command yarnpkg "$@"
|
||||
@@ -1,3 +0,0 @@
|
||||
#! /usr/bin/env bash
|
||||
# This file was generated by nix-binstubs
|
||||
exec nix run -f "$(dirname "$0")/../../../nix" nodejs-8_x --command node "$@"
|
||||
@@ -1,3 +0,0 @@
|
||||
#! /usr/bin/env bash
|
||||
# This file was generated by nix-binstubs
|
||||
exec nix run -f "$(dirname "$0")/../../../nix" nodejs-8_x --command npm "$@"
|
||||
Loaded 100 of 105 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user