Compare commits

...
70 changed files with 433 additions and 954 deletions

No files matched your search

+3 -7
View File
@@ -1,21 +1,17 @@
version: 2
machine:
node:
# Circle will use node v4.0.0
version: 4.0.0
jobs:
build:
docker:
- image: circleci/node:10.15.0
- image: circleci/node:8.12
steps:
- checkout
- run: yarn
- run: yarn lint
- run: yarn build
test:
docker:
- image: circleci/node:10.15.0
- image: circleci/node:8.12
steps:
- checkout
- run: yarn
-21
View File
@@ -1,21 +0,0 @@
{
"plugins": [
"import"
],
"extends": ["universe", "plugin:import/errors"],
"rules":{
"import/order": [
"error",
{
"groups": ["builtin", "external", "parent", "sibling", "index"],
"newlines-between": "always"
}
],
"import/no-unresolved": "off"
},
"settings": {
"import/resolver": {
"babel-module": {}
}
}
}
+25
View File
@@ -0,0 +1,25 @@
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
## [0.5.1] - 2019-01-16
### Changed
- Update Android shell app for SDK32 that includes react-native-screens@1.0.0-alpha.22
## [0.5.0] - 2019-01-16
### Added
- Turtle version is added to onFinish and onError build messages.
### Removed
- `--push-p12-path` parameter for `turtle build:ios` command,
a Push Notifications Certificate is no longer required to perform a successful
iOS standalone app build.
- new/old Turtle version build argument
## [0.4.3] - 2019-01-09
### Changed
- Update iOS shell app for SDK32.
- Update Android shell app for SDK32.
+7
View File
@@ -0,0 +1,7 @@
# Community Code of Conduct
The Expo team is committed to fostering an open and welcoming environment.
**Our Community Guidelines can be found here:**
https://expo.io/guidelines
+21
View File
@@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2015-present 650 Industries, Inc. (aka Expo)
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+13 -1
View File
@@ -1,6 +1,11 @@
# Turtle CLI
Turtle CLI is a command line interface for building Expo standalone apps. You can use it both on your CI and your private computer.
[![License](https://img.shields.io/badge/license-MIT-green.svg?style=flat)](https://github.com/expo/turtle/blob/master/LICENSE)
[![NPM](https://img.shields.io/npm/v/turtle-cli/latest.svg)](https://rubygems.org/gems/fastlane)
[![CircleCI](https://img.shields.io/circleci/project/github/expo/turtle/master.svg)](https://circleci.com/gh/expo/turtle)
Turtle CLI is a command line interface for building Expo standalone apps.
You can use it both on your CI and your private computer.
### Glossary
@@ -11,3 +16,10 @@ Turtle CLI is a command line interface for building Expo standalone apps. You ca
## Installation
[Installation instructions and documentation here.](https://docs.expo.io/versions/latest/distribution/turtle-cli)
## Usage example
See the [expo/turtle-cli-example](https://github.com/expo/turtle-cli-example) repository to learn how to use Turtle CLI in order to build Expo standalone apps on your CI.
## License
The Turtle CLI source code is made available under the [MIT license](LICENSE).
Some of the dependencies are licensed differently, with the BSD license, for example.
+2 -6
View File
@@ -1,6 +1,6 @@
{
"name": "turtle-cli",
"version": "0.4.3",
"version": "0.5.1",
"description": "The command-line tool for building standalone Expo apps",
"preferGlobal": true,
"scripts": {
@@ -9,7 +9,7 @@
"start:android": "PLATFORM=android yarn start",
"watch": "EXPENV_OVERRIDES=local-common,local expenv gulp watch:source",
"bin:turtle": "./bin/turtle.js",
"lint": "eslint src",
"lint": "tslint -c tslint.json 'src/**/*.ts'",
"prepare": "yarn build",
"build": "gulp build:server",
"build:production": "./scripts/build.sh",
@@ -75,13 +75,9 @@
"@types/which": "^1.3.1",
"babel-cli": "^6.26.0",
"babel-core": "^6.26.3",
"babel-eslint": "^8.2.3",
"babel-jest": "^23.6.0",
"babel-plugin-module-resolver": "^3.1.1",
"babel-preset-env": "^1.6.1",
"eslint": "^4.19.1",
"eslint-config-universe": "^1.0.7",
"eslint-import-resolver-babel-module": "^4.0.0",
"gulp": "github:gulpjs/gulp#4.0",
"gulp-babel": "^7.0.1",
"gulp-changed": "^3.2.0",
+1 -1
View File
@@ -1 +1 @@
s3://exp-artifacts/android-shell-builder-55e2cc952dc8bae3ebba8aaaab6eb1199ac7b0d0.tar.gz
s3://exp-artifacts/android-shell-builder-cb03fdc12dc74f699a6d3f22219cbcd30e01ac46.tar.gz
+4 -5
View File
@@ -3,14 +3,13 @@ import fs from 'fs-extra';
import nock from 'nock';
import config from 'turtle/config';
import { PLATFORMS } from 'turtle/constants';
import * as jobManager from 'turtle/jobManager';
import logger from 'turtle/logger';
import { PLATFORMS } from 'turtle/constants';
jest.mock('../builders/utils/uploader.ts', () => ({
uploadBuildToS3: jest.fn(() => {
console.log('Faking uploading build');
logger.debug('Faking uploading build');
return 'legit_path';
}),
}));
@@ -68,11 +67,11 @@ function mockFinish() {
.post('/', (body: any) => {
const keys = Object.keys(body);
if (keys.includes('MessageBody')) {
const mb = JSON.parse(body['MessageBody']);
const mb = JSON.parse(body.MessageBody);
if (mb.status === 'finished') {
return true;
} else if (mb.status === 'errored') {
fail('Build ended with error')
fail('Build ended with error');
}
}
return false;
+29 -23
View File
@@ -1,16 +1,22 @@
import aws from 'aws-sdk';
import _ from 'lodash';
import { EXTRA_DIMENSIONS, GROUP_HASH } from 'turtle/aws/cloudwatch/constants';
import calculateStatistics from 'turtle/aws/cloudwatch/statistics';
import {
IMetric,
IMetricConfiguration,
IMetricData,
IMetricsChunk,
MetricRegistrationObject,
} from 'turtle/aws/cloudwatch/types';
import config from 'turtle/config';
import logger from 'turtle/logger';
import calculateStatistics from 'turtle/aws/cloudwatch/statistics';
import { EXTRA_DIMENSIONS, GROUP_HASH } from 'turtle/aws/cloudwatch/constants';
import { Metric, MetricData, MetricConfiguration, MetricRegistrationObject, MetricsChunk } from 'turtle/aws/cloudwatch/types';
interface State {
metrics: Array<Metric>;
interface IState {
metrics: IMetric[];
registeredMetrics: {
[key: string]: MetricConfiguration;
[key: string]: IMetricConfiguration;
};
intervalId: NodeJS.Timer | null;
}
@@ -19,7 +25,7 @@ const SUCCESS_SUFFIX = '.success';
const FAIL_SUFFIX = '.fail';
const CHUNK_SIZE = 20;
const state: State = {
const state: IState = {
metrics: [],
registeredMetrics: {},
intervalId: null,
@@ -46,7 +52,7 @@ export function addMetricData({
success = true,
explicitName = false,
extraDimensions = [],
}: MetricData) {
}: IMetricData) {
if (config.cloudwatch.disabled) {
return;
}
@@ -57,7 +63,7 @@ export function addMetricData({
return;
}
const { unit } = registeredMetric;
const metric: Metric = {
const metric: IMetric = {
MetricName: metricName,
Unit: unit,
Value: value,
@@ -114,7 +120,7 @@ function _dumpMetrics() {
logger.trace(
'[cloudwatch] lenghts for state.metrics=%d, state.registeredMetrics=%d',
state.metrics.length,
Object.keys(state.registeredMetrics).length
Object.keys(state.registeredMetrics).length,
);
_addEmptyMetrics(state.metrics);
@@ -124,7 +130,7 @@ function _dumpMetrics() {
state.metrics = [];
}
function _addEmptyMetrics(metrics: Array<Metric>) {
function _addEmptyMetrics(metrics: IMetric[]) {
const seenMetrics = metrics.reduce((acc, i) => {
if (!acc.has(i.MetricName)) {
acc.add(i.MetricName);
@@ -134,44 +140,44 @@ function _addEmptyMetrics(metrics: Array<Metric>) {
const nonEmptyMetrics = _.omitBy(state.registeredMetrics, ({ addEmpty }) => !addEmpty);
const toAdd = _.difference(Object.keys(nonEmptyMetrics), Array.from(seenMetrics));
toAdd.forEach(name => addMetricData({ name, value: 0, explicitName: true }));
toAdd.forEach((name) => addMetricData({ name, value: 0, explicitName: true }));
}
function _formatMetrics(metrics: Array<Metric>) {
function _formatMetrics(metrics: IMetric[]) {
const reducedMetrics = _reduceMetrics(metrics);
const metricsWithDimensions = reducedMetrics.map(metric => {
const metricsWithDimensions = reducedMetrics.map((metric) => {
const { dimensions } = state.registeredMetrics[metric.MetricName];
const joinedDimensions = [...dimensions, ...metric[EXTRA_DIMENSIONS]];
return joinedDimensions.map(i => Object.assign({}, metric, { Dimensions: i }));
return joinedDimensions.map((i) => Object.assign({}, metric, { Dimensions: i }));
});
const metricData = _.flatten(metricsWithDimensions);
const chunks = _.chunk(metricData, CHUNK_SIZE);
return chunks.map(chunk => ({
return chunks.map((chunk) => ({
Namespace: config.cloudwatch.namespace,
MetricData: chunk,
}));
}
function _reduceMetrics(metrics: Array<Metric>) {
function _reduceMetrics(metrics: IMetric[]) {
const grouped = _.groupBy(metrics, GROUP_HASH);
return _.reduce(
grouped,
(acc: Array<Metric>, val: any) => {
const { MetricName: key } = val[0] as Metric;
(acc: IMetric[], val: any) => {
const { MetricName: key } = val[0] as IMetric;
const { reducer, statistics } = state.registeredMetrics[key];
const reduced = reducer ? reducer(val) : val;
const withStats = statistics ? calculateStatistics(reduced) : reduced;
acc.push(...withStats);
return acc;
},
[]
[],
);
}
function _pushMetrics(metrics: Array<MetricsChunk>) {
function _pushMetrics(metrics: IMetricsChunk[]) {
logger.trace('[cloudwatch] pushing metrics', JSON.stringify(metrics, null, '\t'));
metrics.forEach(metricsChunk => {
cloudWatch.putMetricData(metricsChunk, err => {
metrics.forEach((metricsChunk) => {
cloudWatch.putMetricData(metricsChunk, (err) => {
if (err) {
logger.warn('[cloudwatch]', err);
}
+5 -5
View File
@@ -1,10 +1,10 @@
import { Metric } from 'turtle/aws/cloudwatch/types';
import { IMetric } from 'turtle/aws/cloudwatch/types';
export function sum(metrics: Array<Metric>) {
export function sum(metrics: IMetric[]) {
if (metrics.length === 0) {
return [];
}
const reduced = metrics.reduce((acc: Metric | null, i: Metric) => {
const reduced = metrics.reduce((acc: IMetric | null, i: IMetric) => {
let result = acc;
if (!result) {
result = {...i};
@@ -16,12 +16,12 @@ export function sum(metrics: Array<Metric>) {
return [reduced];
}
export function avg(metrics: Array<Metric>) {
export function avg(metrics: IMetric[]) {
if (!metrics.length) {
return [];
}
const metric = sum(metrics)[0] as Metric;
const metric = sum(metrics)[0] as IMetric;
metric.Value = metric.Value / metrics.length;
return [metric];
}
+3 -3
View File
@@ -1,11 +1,11 @@
import { Metric, MetricWithStats } from 'turtle/aws/cloudwatch/types';
import { IMetric, MetricWithStats } from 'turtle/aws/cloudwatch/types';
export default function calculateStatistics(metrics: Array<Metric>) {
export default function calculateStatistics(metrics: IMetric[]) {
if (!metrics.length) {
return [];
}
const reduced = metrics.reduce((acc: MetricWithStats | null, i: Metric) => {
const reduced = metrics.reduce((acc: MetricWithStats | null, i: IMetric) => {
let result = acc;
if (!result) {
result = Object.assign({}, i, {
+12 -12
View File
@@ -1,6 +1,6 @@
import { EXTRA_DIMENSIONS, GROUP_HASH } from 'turtle/aws/cloudwatch/constants';
export interface Metric {
export interface IMetric {
MetricName: string;
Unit: string;
Value: number;
@@ -11,8 +11,8 @@ export interface Metric {
SampleCount: number;
Sum: number;
};
[EXTRA_DIMENSIONS]: any,
[GROUP_HASH]: string,
[EXTRA_DIMENSIONS]: any;
[GROUP_HASH]: string;
}
export type MetricWithStats = {
@@ -21,10 +21,10 @@ export type MetricWithStats = {
Minimum: number;
SampleCount: number;
Sum: number;
}
} & Metric;
},
} & IMetric;
export interface MetricData {
export interface IMetricData {
name: string;
value: number;
success?: boolean;
@@ -32,17 +32,17 @@ export interface MetricData {
extraDimensions?: any;
}
export interface MetricConfiguration {
export interface IMetricConfiguration {
unit: string;
statistics?: boolean;
reducer?: any;
addEmpty?: boolean;
dimensions?: any;
};
}
export type MetricRegistrationObject = { metricName: string, explicitName?: boolean } & MetricConfiguration;
export type MetricRegistrationObject = { metricName: string, explicitName?: boolean } & IMetricConfiguration;
export interface MetricsChunk {
export interface IMetricsChunk {
Namespace: string;
MetricData: Array<Metric>;
};
MetricData: IMetric[];
}
+3 -3
View File
@@ -1,10 +1,10 @@
import AWS from 'aws-sdk';
import _ from 'lodash';
import fs from 'fs-extra';
import _ from 'lodash';
import config from 'turtle/config';
interface FileUploadParams {
interface IFileUploadParams {
bucketName?: string;
key: string;
srcPath: string;
@@ -17,7 +17,7 @@ const s3 = new AWS.S3({
export default s3;
export async function uploadFile({ bucketName = config.s3.bucket, key, srcPath }: FileUploadParams) {
export async function uploadFile({ bucketName = config.s3.bucket, key, srcPath }: IFileUploadParams) {
return await s3
.upload({
Bucket: bucketName,
+4 -4
View File
@@ -2,11 +2,11 @@ import retry from 'async-retry';
import AWS from 'aws-sdk';
import _ from 'lodash';
import { OUTPUT_QUEUE_URL, QUEUE_URL } from 'turtle/aws/utils';
import config from 'turtle/config';
import { JOB_UPDATE_TYPE } from 'turtle/constants/build';
import logger from 'turtle/logger';
import { getCurrentJobId } from 'turtle/turtleContext';
import { QUEUE_URL, OUTPUT_QUEUE_URL } from 'turtle/aws/utils';
const sqs = new AWS.SQS({
apiVersion: '2012-11-05',
@@ -26,7 +26,7 @@ export async function receiveMessage(priority: string) {
return sqs
.receiveMessage(params)
.promise()
.then(data => {
.then((data) => {
if (data.Messages) {
logger.info(`Received a message`);
return data.Messages[0];
@@ -35,7 +35,7 @@ export async function receiveMessage(priority: string) {
return null;
}
})
.catch(err => {
.catch((err) => {
logger.error('Receive error', err);
throw err;
});
@@ -67,7 +67,7 @@ export async function changeMessageVisibility(priority: string, receiptHandle: s
export function changeMessageVisibilityRecurring(priority: string, receiptHandle: string, jobId: string) {
return setInterval(() => {
if (getCurrentJobId() === jobId) {
changeMessageVisibility(priority, receiptHandle).catch(err => {
changeMessageVisibility(priority, receiptHandle).catch((err) => {
logger.warn('Error at change msg visibility', err);
});
}
+2
View File
@@ -0,0 +1,2 @@
export class ErrorWithCommandHelp extends Error {
}
+2
View File
@@ -0,0 +1,2 @@
export class ErrorWithProgramHelp extends Error {
}
+9 -8
View File
@@ -1,11 +1,11 @@
import _ from 'lodash';
import fs from 'fs-extra';
import _ from 'lodash';
import uuid from 'uuid';
import builder from 'turtle/builders/android';
import { ErrorWithCommandHelp } from 'turtle/bin/commands/errors';
import { PLATFORMS } from 'turtle/constants';
import { ErrorWithCommandHelp } from 'turtle/bin/commands/ErrorWithCommandHelp';
import { createBuilderAction } from 'turtle/bin/utils/builder';
import builder from 'turtle/builders/android';
import { PLATFORMS } from 'turtle/constants';
export default (program: any, setCommonCommandOptions: any) => {
const command = program.command('build:android [project-dir]');
@@ -14,11 +14,12 @@ export default (program: any, setCommonCommandOptions: any) => {
.alias('ba')
.option(
'--keystore-path <app.jks>',
'path to your Keystore (please provide Keystore password and Key password as EXPO_ANDROID_KEYSTORE_PASSWORD and EXPO_ANDROID_KEY_PASSWORD env variables)'
'path to your Keystore (please provide Keystore password and Key password as EXPO_ANDROID_KEYSTORE_PASSWORD'
+ 'and EXPO_ANDROID_KEY_PASSWORD env variables)',
)
.option('--keystore-alias <alias>', 'keystore Alias')
.description(
'Build a standalone APK for your project, signed and ready for submission to the Google Play Store.'
'Build a standalone APK for your project, signed and ready for submission to the Google Play Store.',
)
.asyncAction(
createBuilderAction({
@@ -29,7 +30,7 @@ export default (program: any, setCommonCommandOptions: any) => {
builder,
platform: PLATFORMS.ANDROID,
os: ['darwin', 'linux'],
})
}),
);
};
@@ -57,7 +58,7 @@ const prepareCredentials = async (cmd: any) => {
if (!credentialsExist) {
if (someCredentialsExist) {
throw new ErrorWithCommandHelp(
'Please provide all required credentials - Keystore (with password), Keystore alias and Key password'
'Please provide all required credentials - Keystore (with password), Keystore alias and Key password',
);
} else {
return null;
+4 -4
View File
@@ -1,5 +1,5 @@
import { default as buildIOS } from 'turtle/bin/commands/build/ios';
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) => {
@@ -15,15 +15,15 @@ function setCommonCommandOptions(cmd: any) {
.option('-o --output <output-file-path>', 'output file path')
.option(
'--public-url <url>',
'the URL of an externally hosted manifest (for self-hosted apps).'
'the URL of an externally hosted manifest (for self-hosted apps).',
)
.option(
'--release-channel <channel-name>',
'pull from specified release channel (default: default)'
'pull from specified release channel (default: default)',
);
}
export default {
ios: createCommand(buildIOS),
android: createCommand(buildAndroid),
}
};
+12 -18
View File
@@ -1,11 +1,11 @@
import fs from 'fs-extra';
import _ from 'lodash';
import uuid from 'uuid';
import fs from 'fs-extra';
import builder from 'turtle/builders/ios';
import { ErrorWithCommandHelp } from 'turtle/bin/commands/errors';
import { IOS, PLATFORMS } from 'turtle/constants';
import { ErrorWithCommandHelp } from 'turtle/bin/commands/ErrorWithCommandHelp';
import { createBuilderAction } from 'turtle/bin/utils/builder';
import builder from 'turtle/builders/ios';
import { IOS, PLATFORMS } from 'turtle/constants';
export default (program: any, setCommonCommandOptions: any) => {
const command = program.command('build:ios [project-dir]');
@@ -16,20 +16,16 @@ export default (program: any, setCommonCommandOptions: any) => {
'-t --type <build>',
'type of build: archive|simulator',
/^(archive|simulator)$/i,
'archive'
'archive',
)
.option('--team-id <apple-teamId>', 'Apple Team ID')
.option(
'--dist-p12-path <dist.p12>',
'path to your Distribution Certificate P12 (please provide password as EXPO_IOS_DIST_P12_PASSWORD env variable)'
)
.option(
'--push-p12-path <push.p12>',
'path to your Push Notification Certificate P12 (please provide password as EXPO_IOS_PUSH_P12_PASSWORD env variable)'
'path to your Distribution Certificate P12 (please provide password as EXPO_IOS_DIST_P12_PASSWORD env variable)',
)
.option('--provisioning-profile-path <.mobileprovision>', 'path to your Provisioning Profile')
.description(
'Build a standalone IPA for your project, signed and ready for submission to the Apple App Store.'
'Build a standalone IPA for your project, signed and ready for submission to the Apple App Store.',
)
.asyncAction(
createBuilderAction({
@@ -40,7 +36,7 @@ export default (program: any, setCommonCommandOptions: any) => {
builder,
platform: PLATFORMS.IOS,
os: 'darwin',
})
}),
);
};
@@ -64,16 +60,16 @@ const prepareCredentials = async (cmd: any) => {
return null;
}
const { teamId, distP12Path, pushP12Path, provisioningProfilePath } = cmd;
const { teamId, distP12Path, provisioningProfilePath } = cmd;
const certPassword = process.env.EXPO_IOS_DIST_P12_PASSWORD;
const pushPassword = process.env.EXPO_IOS_PUSH_P12_PASSWORD;
const credentialsExist =
teamId && distP12Path && certPassword && pushP12Path && pushPassword && provisioningProfilePath;
teamId && distP12Path && certPassword && provisioningProfilePath;
if (!credentialsExist) {
throw new ErrorWithCommandHelp(
'Please provide all required credentials - Apple Team ID, Distribution Certificate P12 (with password), Push Notification Certificate P12 (with password) and Provisioning Profile'
'Please provide all required credentials'
+ '- Apple Team ID, Distribution Certificate P12 (with password) and Provisioning Profile',
);
}
@@ -81,8 +77,6 @@ const prepareCredentials = async (cmd: any) => {
teamId,
certP12: (await fs.readFile(distP12Path)).toString('base64'),
certPassword,
pushP12: (await fs.readFile(pushP12Path)).toString('base64'),
pushPassword,
provisioningProfile: (await fs.readFile(provisioningProfilePath)).toString('base64'),
};
};
-2
View File
@@ -1,2 +0,0 @@
export class ErrorWithProgramHelp extends Error {}
export class ErrorWithCommandHelp extends Error {}
+5 -4
View File
@@ -1,9 +1,10 @@
import _ from 'lodash';
import logger from 'turtle/logger';
import { ErrorWithCommandHelp } from 'turtle/bin/commands/ErrorWithCommandHelp';
import { ErrorWithProgramHelp } from 'turtle/bin/commands/ErrorWithProgramHelp';
import setup from 'turtle/bin/setup/setup';
import { ErrorWithCommandHelp, ErrorWithProgramHelp } from 'turtle/bin/commands/errors';
import { PLATFORMS } from 'turtle/constants';
import logger from 'turtle/logger';
const l = logger.withFields({ buildPhase: 'setting up environment' });
@@ -16,7 +17,7 @@ function createSetupCommand(platform: string, os?: string) {
.description(`Setup environment for building ${_.capitalize(platform)} standalone apps.`)
.option(
'--sdk-version <sdk-version>',
'version of Expo SDK shell app to download (optional)'
'version of Expo SDK shell app to download (optional)',
)
.asyncAction(async (cmd: any) => await setupAction(program, cmd, platform, os));
};
@@ -30,7 +31,7 @@ async function setupAction(program: any, cmd: any, platform: string, os?: string
const { sdkVersion } = cmd;
await setup(platform, sdkVersion);
l.info('it\'s all set!')
l.info('it\'s all set!');
} catch (err) {
logger.error(`Failed to setup environment for ${platform} builds`);
logger.error(err.stack);
+3 -3
View File
@@ -1,5 +1,5 @@
import path from 'path';
import os from 'os';
import path from 'path';
import _ from 'lodash';
@@ -16,8 +16,8 @@ const resolveEnv = () => {
};
const currentEnv = resolveEnv();
const isLocal = (env: string) => env === 'local';
const isStaging = (env: string) => env === 'staging';
const isLocal = (envName: string) => envName === 'local';
const isStaging = (envName: string) => envName === 'staging';
const apiConfig = {
protocol: 'https',
+18 -14
View File
@@ -1,11 +1,13 @@
import { LoggerDetach, ModuleVersion } from 'xdl';
import path from 'path';
import program, { Command } from 'commander';
import _ from 'lodash';
import fs from 'fs-extra';
import { LoggerDetach, ModuleVersion } from 'xdl';
import logger from 'turtle/logger';
import * as commands from 'turtle/bin/commands';
import logger from 'turtle/logger';
const { name, version } = require('../../package.json');
const { name, version } = JSON.parse(fs.readFileSync(path.join(__dirname, '../../package.json'), 'utf-8'));
const ModuleVersionChecker = ModuleVersion.createModuleVersionChecker(name, version);
@@ -15,13 +17,15 @@ Command.prototype.asyncAction = function asyncAction(asyncFn: (...asyncFnArgs: a
return this.action(async (...args) => {
try {
await checkForUpdateAsync();
} catch (e) {}
} catch (e) {
logger.warn('Failed to check for turtle-cli update.');
}
return await asyncFn(...args);
});
}
};
export function run(programName: string) {
runAsync(programName).catch(e => {
runAsync(programName).catch((e) => {
logger.error('Uncaught Error', e);
process.exit(1);
});
@@ -29,22 +33,22 @@ export function run(programName: string) {
async function runAsync(programName: string) {
program.version(version);
Object.values(commands).forEach(command => registerCommand(program, command));
Object.values(commands).forEach((command) => registerCommand(program, command));
program.parse(process.argv);
const subCommand = process.argv[2];
if (subCommand) {
const commands = program.commands.reduce((acc: Array<string>, command: any) => {
acc.push(command['_name']);
const alias = command['_alias'];
const commandNames = program.commands.reduce((acc: string[], command: any) => {
acc.push(command._name);
const alias = command._alias;
if (alias) {
acc.push(alias);
}
return acc;
}, []);
if (!_.includes(commands, subCommand)) {
if (!commandNames.includes(subCommand)) {
logger.error(
`"${subCommand}" is not an ${programName} command. See "${programName} --help" for the full list of commands.`
`"${subCommand}" is not an ${programName} command. See "${programName} --help" for the full list of commands.`,
);
}
} else {
@@ -60,7 +64,7 @@ async function checkForUpdateAsync() {
logger.warn(
`There is a new version of ${name} available (${latest}).
You are currently using ${name} ${current}
Run \`npm install -g ${name}\` to get the latest version`
Run \`npm install -g ${name}\` to get the latest version`,
);
}
}
+12 -12
View File
@@ -2,20 +2,20 @@ import path from 'path';
import util from 'util';
import fs from 'fs-extra';
import { ExponentTools } from 'xdl';
import _which from 'which';
import { ExponentTools } from 'xdl';
import logger from 'turtle/logger';
import config from 'turtle/config';
import { IToolDefinition } from 'turtle/bin/setup/utils/toolsDetector';
import { checkSystem, ensureShellAppIsPresent } from 'turtle/bin/setup/utils/common';
import { formatShellAppDirectory } from 'turtle/builders/utils/android/workingdir';
import ensureAndroidSDKIsPresent from 'turtle/bin/setup/android/sdk';
import ensureAndroidNDKIsPresent from 'turtle/bin/setup/android/ndk';
import ensureAndroidSDKIsPresent from 'turtle/bin/setup/android/sdk';
import { checkSystem, ensureShellAppIsPresent } from 'turtle/bin/setup/utils/common';
import { IToolDefinition } from 'turtle/bin/setup/utils/toolsDetector';
import { formatShellAppDirectory } from 'turtle/builders/utils/android/workingdir';
import config from 'turtle/config';
import { PLATFORMS } from 'turtle/constants';
import logger from 'turtle/logger';
const which = util.promisify(_which);
const REQUIRED_TOOLS: Array<IToolDefinition> = [
const REQUIRED_TOOLS: IToolDefinition[] = [
{
command: 'bash',
missingDescription: 'Please install bash',
@@ -31,13 +31,13 @@ const REQUIRED_TOOLS: Array<IToolDefinition> = [
const { status, stdout } = await ExponentTools.spawnAsyncThrowError(
'javac',
['-version'],
{ stdio: 'pipe' }
{ stdio: 'pipe' },
);
if (stdout.match(/no java runtime present/i)) {
return false;
}
return status === 0;
}
},
},
];
const LOGGER_FIELDS = { buildPhase: 'setting up environment' };
@@ -50,7 +50,7 @@ export default async function setup(sdkVersion?: string) {
await ensureShellAppIsPresent(
sdkVersion,
{ formatShellAppDirectory, formatShellAppTarballUriPath },
_shellAppPostDownloadAction
_shellAppPostDownloadAction,
);
}
}
@@ -107,7 +107,7 @@ function _setEnvVars(envVars: object) {
.forEach(([key, value]) => process.env[key] = value);
}
function _alterPath(newPaths: Array<string>) {
function _alterPath(newPaths: string[]) {
const currentPaths = process.env.PATH ? process.env.PATH.split(':') : [];
const paths = [...newPaths, ...currentPaths];
process.env.PATH = paths.join(':');
+4 -4
View File
@@ -4,10 +4,10 @@ import path from 'path';
import decompress from 'decompress';
import fs from 'fs-extra';
import { formatArtifactDownloadPath } from 'turtle/bin/setup/utils/common';
import download from 'turtle/bin/setup/utils/downloader';
import config from 'turtle/config';
import logger from 'turtle/logger';
import download from 'turtle/bin/setup/utils/downloader';
import { formatArtifactDownloadPath } from 'turtle/bin/setup/utils/common';
const ANDROID_NDK_URL = `https://dl.google.com/android/repository/android-ndk-r10e-${os.platform()}-x86_64.zip`;
@@ -24,7 +24,7 @@ export default async function ensureAndroidNDKIsPresent() {
l.info('Downloading Android NDK');
await fs.ensureDir(config.directories.artifactsDir);
await download(ANDROID_NDK_URL, androidNdkDownloadPath);
l.info('Decompressing Android NDK')
l.info('Decompressing Android NDK');
await decompress(androidNdkDownloadPath, androidNdkDir, { strip: 1 });
l.info('Android NDK installed successfully');
} catch (err) {
@@ -47,5 +47,5 @@ function _createEnvVars(androidNdkDir: string) {
}
function _createPaths(androidNdkDir: string) {
return [androidNdkDir]
return [androidNdkDir];
}
+3 -3
View File
@@ -5,10 +5,10 @@ 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';
import download from 'turtle/bin/setup/utils/downloader';
import { formatArtifactDownloadPath } from 'turtle/bin/setup/utils/common';
const ANDROID_SDK_URL = 'https://dl.google.com/android/repository/sdk-tools-linux-3859397.zip';
@@ -26,7 +26,7 @@ export default async function ensureAndroidSDKIsPresent() {
await fs.ensureDir(config.directories.artifactsDir);
await download(ANDROID_SDK_URL, androidSdkDownloadPath);
await fs.ensureDir(androidSdkDir);
l.info('Decompressing Android SDK')
l.info('Decompressing Android SDK');
await decompress(androidSdkDownloadPath, androidSdkDir);
await fs.remove(androidSdkDownloadPath);
l.info('Configuring Android SDK, this may take a while');
+4 -4
View File
@@ -2,13 +2,13 @@ import path from 'path';
import { ExponentTools } from 'xdl';
import config from 'turtle/config';
import { checkSystem, ensureShellAppIsPresent } from 'turtle/bin/setup/utils/common';
import { IToolDefinition } from 'turtle/bin/setup/utils/toolsDetector';
import { formatShellAppDirectory } from 'turtle/builders/utils/ios/workingdir';
import config from 'turtle/config';
import { PLATFORMS } from 'turtle/constants';
const REQUIRED_TOOLS: Array<IToolDefinition> = [
const REQUIRED_TOOLS: IToolDefinition[] = [
{
command: 'bash',
missingDescription: 'Please install bash',
@@ -24,13 +24,13 @@ const REQUIRED_TOOLS: Array<IToolDefinition> = [
const { status, stdout } = await ExponentTools.spawnAsyncThrowError(
'xcodebuild',
['-version'],
{ stdio: 'pipe' }
{ stdio: 'pipe' },
);
if (stdout.match(/requires xcode/i)) {
return false;
}
return status === 0;
}
},
},
];
+1 -1
View File
@@ -1,7 +1,7 @@
import isSemver from 'is-semver';
import setupIos from 'turtle/bin/setup/ios';
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) {
+11 -9
View File
@@ -1,14 +1,13 @@
import path from 'path';
import { ExponentTools } from 'xdl';
import fs from 'fs-extra';
import tar from 'tar';
import config from 'turtle/config';
import logger from 'turtle/logger';
import { IToolDefinition, ensureToolsAreInstalled } from 'turtle/bin/setup/utils/toolsDetector';
import { ExponentTools } from 'xdl';
import download from 'turtle/bin/setup/utils/downloader';
import { ensureToolsAreInstalled, IToolDefinition } from 'turtle/bin/setup/utils/toolsDetector';
import config from 'turtle/config';
import logger from 'turtle/logger';
const l = logger.withFields({ buildPhase: 'setting up environment' });
@@ -19,11 +18,15 @@ interface IShellAppFormaters {
type PostDownloadAction = (workingdir: string) => void;
export async function checkSystem(requiredTools: Array<IToolDefinition>) {
export async function checkSystem(requiredTools: IToolDefinition[]) {
await ensureToolsAreInstalled(requiredTools);
}
export async function ensureShellAppIsPresent(sdkVersion: string, formatters: IShellAppFormaters, postDownloadAction?: PostDownloadAction) {
export async function ensureShellAppIsPresent(
sdkVersion: string,
formatters: IShellAppFormaters,
postDownloadAction?: PostDownloadAction,
) {
const workingdir = formatters.formatShellAppDirectory(sdkVersion);
if (await fs.pathExists(workingdir)) {
return;
@@ -45,12 +48,11 @@ async function _downloadShellApp(sdkVersion: string, targetDirectory: string, fo
l.info('extracting shell app (this may take a while)...');
await tar.x({
file: tarballDownloadTargetPath,
C: targetDirectory
C: targetDirectory,
});
l.info('shell app extracted');
}
export function formatArtifactDownloadPath(uri: string) {
const { base } = path.parse(uri);
return path.join(config.directories.artifactsDir, base);
+2 -3
View File
@@ -1,9 +1,9 @@
import { URL } from 'url';
import fs from 'fs';
import { URL } from 'url';
import ProgressBar from 'progress';
import request from 'request';
import requestProgress from 'request-progress';
import ProgressBar from 'progress';
const S3_PROTOCOL = 's3:';
const PROGRESS_BAR_CONFIG = {
@@ -38,7 +38,6 @@ export default async function download(url: string, destPath: string) {
});
}
function convertS3ToHttpsUrl(s3UrlRaw: string) {
const { host: s3Bucket, pathname: s3Path, protocol } = new URL(s3UrlRaw);
if (protocol !== S3_PROTOCOL) {
+1 -1
View File
@@ -12,7 +12,7 @@ export interface IToolDefinition {
testFn?: () => Promise<boolean>;
}
export async function ensureToolsAreInstalled(tools: Array<IToolDefinition>) {
export async function ensureToolsAreInstalled(tools: IToolDefinition[]) {
let isAnyToolMissing = false;
for (const { command, missingDescription, testFn } of tools) {
try {
+11 -7
View File
@@ -1,11 +1,12 @@
import _ from 'lodash';
import { ErrorWithCommandHelp } from 'turtle/bin/commands/ErrorWithCommandHelp';
import { ErrorWithProgramHelp } from 'turtle/bin/commands/ErrorWithProgramHelp';
import setup from 'turtle/bin/setup/setup';
import * as ProjectUtils from 'turtle/bin/utils/project';
import * as UserUtils from 'turtle/bin/utils/user';
import logger from 'turtle/logger';
import { sanitizeJob } from 'turtle/validator';
import * as UserUtils from 'turtle/bin/utils/user';
import * as ProjectUtils from 'turtle/bin/utils/project';
import { ErrorWithCommandHelp, ErrorWithProgramHelp } from 'turtle/bin/commands/errors';
import setup from 'turtle/bin/setup/setup';
export function createBuilderAction({
program,
@@ -29,7 +30,9 @@ export function createBuilderAction({
}
if (cmd.buildDir && cmd.output) {
throw new ErrorWithCommandHelp('You can\'t provide both --build-dir and --output parameters, please choose one');
throw new ErrorWithCommandHelp(
'You can\'t provide both --build-dir and --output parameters, please choose one',
);
}
const userData = {
@@ -44,10 +47,11 @@ export function createBuilderAction({
throw new ErrorWithCommandHelp('You must provide both Expo username and password');
}
} else if (!cmd.publicUrl) {
throw new ErrorWithCommandHelp('You must provide your Expo username and password unless you specify --public-url to your project manifest.');
throw new ErrorWithCommandHelp(
'You must provide your Expo username and password unless you specify --public-url to your project manifest.',
);
}
const args = {
releaseChannel: cmd.releaseChannel || 'default',
buildType: cmd.type,
+1 -1
View File
@@ -1,5 +1,5 @@
import path from 'path';
import fs from 'fs-extra';
import path from 'path';
export async function loadAppJSON(projectDirArg: string) {
const projectDir = resolveAbsoluteDir(projectDirArg);
+1 -1
View File
@@ -1,6 +1,6 @@
import { User as UserManager } from 'xdl';
import { ErrorWithProgramHelp } from 'turtle/bin/commands/errors';
import { ErrorWithProgramHelp } from 'turtle/bin/commands/ErrorWithProgramHelp';
interface IUserData {
username?: string;
+7 -4
View File
@@ -2,17 +2,17 @@ import path from 'path';
import fs from 'fs-extra';
import _ from 'lodash';
import { AndroidShellApp, ImageUtils } from 'xdl';
import uuidv4 from 'uuid/v4';
import { AndroidShellApp, ImageUtils } from 'xdl';
import getOrCreateCredentials from 'turtle/builders/utils/android/credentials';
import { formatShellAppDirectory } from 'turtle/builders/utils/android/workingdir';
import * as commonUtils from 'turtle/builders/utils/common';
import * as imageHelpers from 'turtle/builders/utils/image';
import { uploadBuildToS3 } from 'turtle/builders/utils/uploader';
import config from 'turtle/config';
import logger from 'turtle/logger';
import { IAndroidCredentials, IJob, IJobResult } from 'turtle/job';
import { formatShellAppDirectory } from 'turtle/builders/utils/android/workingdir';
import logger from 'turtle/logger';
export default async function buildAndroid(jobData: IJob): Promise<IJobResult> {
const credentials = await getOrCreateCredentials(jobData);
@@ -25,7 +25,10 @@ export default async function buildAndroid(jobData: IJob): Promise<IJobResult> {
uploadPath: apkFilePath,
s3FileKey,
...config.builder.fakeUpload && {
fakeUploadBuildPath: jobData.fakeUploadBuildPath ? jobData.fakeUploadBuildPath : path.join(jobData.fakeUploadDir || config.directories.fakeUploadDir, fakeUploadFilename)
fakeUploadBuildPath:
jobData.fakeUploadBuildPath
? jobData.fakeUploadBuildPath
: path.join(jobData.fakeUploadDir || config.directories.fakeUploadDir, fakeUploadFilename),
},
});
+1 -1
View File
@@ -1,5 +1,5 @@
import buildIOS from './ios';
import buildAndroid from './android';
import buildIOS from './ios';
export default {
ios: buildIOS,
+6 -6
View File
@@ -1,13 +1,13 @@
import _ from 'lodash';
import { IosIPABuilder as createIPABuilder } from 'xdl';
import runShellAppBuilder from 'turtle/builders/utils/ios/shellAppBuilder';
import { IContext } from 'turtle/builders/ios/context';
import { logErrorOnce } from 'turtle/builders/utils/common';
import * as fileUtils from 'turtle/builders/utils/file';
import * as keychain from 'turtle/builders/utils/ios/keychain';
import logger from 'turtle/logger/index';
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/index';
export default async function buildArchive(ctx: IContext, job: IJob) {
let keychainInfo;
@@ -34,11 +34,11 @@ async function buildAndSignIPA(ctx: IContext, job: IJob, keychainPath: string, m
const {
credentials: { provisioningProfile, certPassword, teamId, password },
config: { bundleIdentifier: _bundleIdentifierFromConfig },
config: { bundleIdentifier: bundleIdentifierFromConfig },
} = job;
const _bundleIdentifierFromManifest = _.get(manifest, 'ios.bundleIdentifier');
const bundleIdentifier = _bundleIdentifierFromConfig || _bundleIdentifierFromManifest;
const bundleIdentifierFromManifest = _.get(manifest, 'ios.bundleIdentifier');
const bundleIdentifier = bundleIdentifierFromConfig || bundleIdentifierFromManifest;
const { provisioningProfilePath } = ctx;
await fileUtils.writeBase64ToBinaryFile(provisioningProfilePath, provisioningProfile as string);
+15 -7
View File
@@ -3,10 +3,10 @@ import * as path from 'path';
import { v4 as uuidv4 } from 'uuid';
import { IosShellApp } from 'xdl';
import { formatShellAppDirectory } from 'turtle/builders/utils/ios/workingdir';
import config from 'turtle/config';
import { IOS, PLATFORMS } from 'turtle/constants/index';
import { IJob } from 'turtle/job';
import { formatShellAppDirectory } from 'turtle/builders/utils/ios/workingdir';
export interface IContext {
appDir: string;
@@ -30,9 +30,15 @@ const { BUILD_TYPES } = IOS;
export function createBuilderContext(job: IJob): IContext {
const { join } = path;
const appUUID = uuidv4();
const { manifest: { sdkVersion: _sdkVersionFromManifest = null } = {}, sdkVersion: _sdkVersionFromJob, config: { buildType } } = job;
const {
manifest: {
sdkVersion: sdkVersionFromManifest = null,
} = {},
sdkVersion: sdkVersionFromJob,
config: { buildType },
} = job;
const sdkVersion = _sdkVersionFromJob || _sdkVersionFromManifest;
const sdkVersion = sdkVersionFromJob || sdkVersionFromManifest;
const workingDir = formatShellAppDirectory(sdkVersion);
const context: any = {
@@ -44,7 +50,7 @@ export function createBuilderContext(job: IJob): IContext {
context.provisioningProfileDir = join(context.appDir, 'provisioning');
context.provisioningProfilePath = join(
context.provisioningProfileDir,
`${appUUID}.mobileprovision`
`${appUUID}.mobileprovision`,
);
context.tempCertPath = join(context.appDir, 'cert.p12');
context.baseArchiveDir = join(context.appDir, 'archive');
@@ -55,7 +61,7 @@ export function createBuilderContext(job: IJob): IContext {
context.archiveDir = join(
context.baseArchiveDir,
'Release',
`${DEFAULT_EXPOKIT_WORKSPACE_NAME}.app`
`${DEFAULT_EXPOKIT_WORKSPACE_NAME}.app`,
);
} else if (buildType === BUILD_TYPES.ARCHIVE) {
context.outputPath = join(context.appDir, 'archive.xcarchive');
@@ -66,7 +72,7 @@ export function createBuilderContext(job: IJob): IContext {
`${DEFAULT_EXPOKIT_WORKSPACE_NAME}.xcarchive`,
'Products',
'Applications',
`${DEFAULT_EXPOKIT_WORKSPACE_NAME}.app`
`${DEFAULT_EXPOKIT_WORKSPACE_NAME}.app`,
);
}
@@ -74,7 +80,9 @@ export function createBuilderContext(job: IJob): IContext {
const s3Filename = `${job.experienceName}-${appUUID}-${buildType}.${s3FileExtension}`;
if (config.builder.fakeUpload) {
const fakeUploadFilename = s3Filename.replace('/', '\\');
context.fakeUploadBuildPath = job.fakeUploadBuildPath ? job.fakeUploadBuildPath : join(job.fakeUploadDir || config.directories.fakeUploadDir, fakeUploadFilename);
context.fakeUploadBuildPath = job.fakeUploadBuildPath
? job.fakeUploadBuildPath
: join(job.fakeUploadDir || config.directories.fakeUploadDir, fakeUploadFilename);
} else {
context.s3FileKey = join(PLATFORMS.IOS, s3Filename);
}
+9 -9
View File
@@ -1,7 +1,7 @@
import * as fs from 'fs-extra';
import * as _ from 'lodash';
import * as path from 'path';
import path from 'path';
import fs from 'fs-extra';
import pick from 'lodash/pick';
import { ExponentTools } from 'xdl';
import buildArchive from 'turtle/builders/ios/archive';
@@ -15,7 +15,7 @@ import { IJob, IJobResult } from 'turtle/job';
const { BUILD_TYPES } = IOS;
let SUPPORTED_SDK_VERSIONS: Array<number>;
let SUPPORTED_SDK_VERSIONS: number[];
export default async function iosBuilder(job: IJob): Promise<IJobResult> {
await ensureCanBuildSdkVersion(job);
@@ -59,7 +59,7 @@ async function cleanup(ctx: IContext) {
}
const getTemporaryDirs = (ctx: IContext) =>
Object.values(_.pick(ctx, ['appDir', 'provisioningProfileDir']));
Object.values(pick(ctx, ['appDir', 'provisioningProfileDir']));
async function ensureCanBuildSdkVersion(job: IJob) {
if (config.builder.useLocalWorkingDir) {
@@ -71,14 +71,14 @@ async function ensureCanBuildSdkVersion(job: IJob) {
}
const targetMajorSdkVersion = ExponentTools.parseSdkMajorVersion(job.sdkVersion || job.manifest.sdkVersion);
if (!_.includes(SUPPORTED_SDK_VERSIONS, targetMajorSdkVersion)) {
if (!SUPPORTED_SDK_VERSIONS.includes(targetMajorSdkVersion)) {
throw new Error(`Unsupported SDK Version!`);
}
}
async function findSupportedSdkVersions(): Promise<Array<number>> {
async function findSupportedSdkVersions(): Promise<number[]> {
const SDK_DIR_PREFIX = 'sdk';
const files = await fs.readdir(path.join(config.directories.workingDir, PLATFORMS.IOS));
const sdks = files.filter(file => file.startsWith(SDK_DIR_PREFIX));
return sdks.map(sdk => parseInt(sdk.substr(SDK_DIR_PREFIX.length)));
const sdks = files.filter((file) => file.startsWith(SDK_DIR_PREFIX));
return sdks.map((sdk) => parseInt(sdk.substr(SDK_DIR_PREFIX.length), 10));
}
+2 -2
View File
@@ -1,7 +1,7 @@
import runShellAppBuilder from 'turtle/builders/utils/ios/shellAppBuilder';
import logger from 'turtle/logger';
import { IContext } from 'turtle/builders/ios/context';
import runShellAppBuilder from 'turtle/builders/utils/ios/shellAppBuilder';
import { IJob } from 'turtle/job';
import logger from 'turtle/logger';
export default async function buildSimulator(ctx: IContext, job: IJob) {
const l = logger.withFields({ buildPhase: 'running simulator builder' });
+2 -3
View File
@@ -1,14 +1,14 @@
import path from 'path';
import uuidv4 from 'uuid/v4';
import fs from 'fs-extra';
import uuidv4 from 'uuid/v4';
import { AndroidKeystore } from 'xdl';
import * as sqs from 'turtle/aws/sqs';
import { UPDATE_CREDENTIALS } from 'turtle/constants/build';
import { IAndroidCredentials, IJob } from 'turtle/job';
import logger from 'turtle/logger';
import { isOffline } from 'turtle/turtleContext';
import { IAndroidCredentials, IJob } from 'turtle/job';
async function getOrCreateCredentials(jobData: IJob): Promise<IAndroidCredentials> {
if (!jobData.credentials) {
@@ -42,7 +42,6 @@ async function getOrCreateCredentials(jobData: IJob): Promise<IAndroidCredential
} else {
await sqs.sendMessage(jobData.id, UPDATE_CREDENTIALS, {
...credentials,
turtleVersion: jobData.config.turtleVersion,
});
l.info('Keystore sent to storage successfully');
}
+3 -1
View File
@@ -1,6 +1,8 @@
import path from 'path';
import config from 'turtle/config';
import { ExponentTools } from 'xdl';
import config from 'turtle/config';
import { PLATFORMS } from 'turtle/constants';
export function formatShellAppDirectory(sdkVersion: string) {
+1 -1
View File
@@ -1,8 +1,8 @@
import * as url from 'url';
import config from 'turtle/config';
import logger from 'turtle/logger';
import { IJob } from 'turtle/job';
import logger from 'turtle/logger';
export function getExperienceUrl(job: IJob) {
const { experienceName, config: jobConfig } = job;
+1 -2
View File
@@ -1,14 +1,13 @@
import { IosKeychain } from 'xdl';
import { IContext } from 'turtle/builders/ios/context';
import * as fileUtils from 'turtle/builders/utils/file';
import logger from 'turtle/logger';
import { IContext } from 'turtle/builders/ios/context';
interface IKeychain {
path: string;
}
export async function create(ctx: IContext): Promise<IKeychain> {
const l = logger.withFields({ buildPhase: 'creating keychain' });
try {
+6 -6
View File
@@ -2,22 +2,22 @@ import path from 'path';
import util from 'util';
import copy from 'copy';
import { ImageUtils, IosShellApp } from 'xdl';
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 logger from 'turtle/logger/index';
import { IContext } from 'turtle/builders/ios/context';
import { IJob } from 'turtle/job';
import { PLATFORMS } from 'turtle/constants';
import { IJob } from 'turtle/job';
import logger from 'turtle/logger/index';
const copyAsync = util.promisify(copy);
export default async function runShellAppBuilder(ctx: IContext, job: IJob): Promise<any> {
const { config: jobConfig, manifest, sdkVersion: _sdkVersionFromJob } = job;
const { config: jobConfig, manifest, sdkVersion: sdkVersionFromJob } = job;
const { buildType, releaseChannel } = jobConfig;
const sdkVersion = _.get(manifest, 'sdkVersion', _sdkVersionFromJob);
const sdkVersion = _.get(manifest, 'sdkVersion', sdkVersionFromJob);
const applicationFilesSrc = path.join(
ctx.workingDir,
'shellAppBase-builds',
+2
View File
@@ -1,5 +1,7 @@
import path from 'path';
import { ExponentTools } from 'xdl';
import config from 'turtle/config';
import { PLATFORMS } from 'turtle/constants';
+10 -4
View File
@@ -1,8 +1,8 @@
import os from 'os';
import path from 'path';
import { env, envNum, envOptional, envTransform } from 'turtle/utils/env';
import { PLATFORMS } from 'turtle/constants';
import { env, envNum, envOptional, envTransform } from 'turtle/utils/env';
export default {
env: env('NODE_ENV'),
@@ -66,7 +66,7 @@ export default {
skipCleanup: envTransform(
'TURTLE_SKIP_CLEANUP',
'0',
(val: string) => env('NODE_ENV') === 'development' && val === '1'
(val: string) => env('NODE_ENV') === 'development' && val === '1',
),
fakeUpload: envTransform('TURTLE_FAKE_UPLOAD', '0', (val: string) => val === '1'),
maxJobTimeMs: envNum('TURTLE_MAX_JOB_TIME_MS', 60 * 60 * 1000),
@@ -77,9 +77,15 @@ export default {
workingDir: env('TURTLE_WORKING_DIR_PATH', path.join(__dirname, '../workingdir')),
shellTarballsDir: env('TURTLE_SHELL_TARBALLS_DIR', path.join(__dirname, '../shellTarballs')),
artifactsDir: env('TURTLE_ARTIFACTS_DIR', path.join(__dirname, '../artifacts')),
androidDependenciesDir: env('TURTLE_ANDROID_DEPENDENCIES_DIR', path.join(os.homedir(), '.turtle/androidDependencies')),
androidDependenciesDir: env(
'TURTLE_ANDROID_DEPENDENCIES_DIR',
path.join(os.homedir(), '.turtle/androidDependencies'),
),
tempS3LogsDir: env('TURTLE_TEMP_S3_LOGS_DIR', '/tmp/logs'),
fakeUploadDir: envOptional('TURTLE_FAKE_UPLOAD_DIR'),
temporaryFilesRoot: env('TURTLE_TEMPORARY_FILES_DIR', path.join(os.platform() === 'darwin' ? '/private' : '/', 'tmp', 'turtle')),
temporaryFilesRoot: env(
'TURTLE_TEMPORARY_FILES_DIR',
path.join(os.platform() === 'darwin' ? '/private' : '/', 'tmp', 'turtle'),
),
},
};
+1 -1
View File
@@ -5,7 +5,7 @@ export enum JOB_STATES {
FINISHED = 'finished',
ERRORED = 'errored',
SENT_TO_QUEUE = 'sent-to-queue',
};
}
export const UPDATE_CREDENTIALS = 'update-credentials';
-6
View File
@@ -7,9 +7,3 @@ export const PLATFORMS = {
};
export { IOS, BUILD };
export const TURTLE_VERSIONS = {
NEW: 'new',
OLD: 'old',
BOTH: 'both',
};
-1
View File
@@ -6,7 +6,6 @@ export interface IJob {
sdkVersion: string;
config: {
releaseChannel: string;
turtleVersion?: string;
publicUrl?: string;
// ios
buildType?: string;
+13 -13
View File
@@ -1,17 +1,22 @@
import path from 'path';
import fs from 'fs-extra';
import _ from 'lodash';
import { logErrorOnce } from 'turtle/builders/utils/common';
import logger from 'turtle/logger';
import * as sqs from 'turtle/aws/sqs';
import { BUILD } from 'turtle/constants/index';
import { sanitizeJob } from 'turtle/validator';
import builders from 'turtle/builders';
import { logErrorOnce } from 'turtle/builders/utils/common';
import config from 'turtle/config';
import * as redis from 'turtle/utils/redis';
import { checkShouldExit, setCurrentJobId } from 'turtle/turtleContext';
import * as buildStatusMetric from 'turtle/metrics/buildStatus';
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 } from 'turtle/turtleContext';
import { getPriorities } from 'turtle/utils/priorities';
import * as redis from 'turtle/utils/redis';
import { sanitizeJob } from 'turtle/validator';
const { version: turtleVersion } = JSON.parse(fs.readFileSync(path.join(__dirname, '../package.json'), 'utf-8'));
function _maybeExit() {
if (checkShouldExit()) {
@@ -29,7 +34,6 @@ export async function doJob() {
export async function getJob() {
_maybeExit();
logger.info('Fetching job');
// eslint-disable-next-line no-constant-condition
while (true) {
try {
let job = null;
@@ -109,9 +113,7 @@ async function processJob(jobData: any) {
function failAfterMaxJobTime(priority: string, receiptHandle: string, job: any) {
return setTimeout(async () => {
try {
sqs.sendMessage(job.id, BUILD.JOB_STATES.ERRORED, {
turtleVersion: job.config.turtleVersion,
});
sqs.sendMessage(job.id, BUILD.JOB_STATES.ERRORED, { turtleVersion });
await deleteMessage(priority, receiptHandle);
} finally {
logger.error('Going to terminate turtle agent, just in case...');
@@ -122,13 +124,11 @@ function failAfterMaxJobTime(priority: string, receiptHandle: string, job: any)
async function build(job: any) {
const s3Url = await logger.init(job);
const { turtleVersion } = job.config;
try {
await sqs.sendMessage(job.id, BUILD.JOB_STATES.IN_PROGRESS, {
logUrl: s3Url,
logFormat: 'json',
turtleVersion,
});
const result = await (builders as any)[job.platform](job);
sqs.sendMessage(job.id, BUILD.JOB_STATES.FINISHED, { ...result, turtleVersion });
+1 -1
View File
@@ -12,5 +12,5 @@ export default baseJobSchema.concat(
keystoreAlias: Joi.string(),
keystore: Joi.string(),
}),
})
}),
);
+1 -2
View File
@@ -1,6 +1,6 @@
import Joi from 'joi';
import { PLATFORMS, TURTLE_VERSIONS } from 'turtle/constants';
import { PLATFORMS } from 'turtle/constants';
export default Joi.object().keys({
platform: Joi.string().valid(Object.values(PLATFORMS)),
@@ -10,7 +10,6 @@ export default Joi.object().keys({
config: Joi.object()
.keys({
releaseChannel: Joi.string().regex(/[a-z\d][a-z\d._-]*/),
turtleVersion: Joi.string().valid(Object.values(TURTLE_VERSIONS)),
})
.unknown(true),
projectDir: Joi.string(),
+1 -1
View File
@@ -23,5 +23,5 @@ export default baseJobSchema.concat(
then: Joi.optional(),
otherwise: Joi.required(),
}),
})
}),
);
+11 -11
View File
@@ -6,36 +6,36 @@ import config from 'turtle/config';
import * as constants from 'turtle/constants/logger';
import { IJob } from 'turtle/job';
export interface LogglyConfig {
export interface ILogglyConfig {
token: string;
subdomain: string;
tags: Array<string>;
tags: string[];
}
interface LogglyExtraFields {
jobID: string,
interface ILogglyExtraFields {
jobID: string;
experienceName: string;
}
export default class HackyLogglyStream extends EventEmitter {
logglyStream: BunyanLoggly;
extraFields: LogglyExtraFields | null;
private logglyStream: BunyanLoggly;
private extraFields: ILogglyExtraFields | null;
constructor(logglyConfig: LogglyConfig) {
constructor(logglyConfig: ILogglyConfig) {
super();
this.logglyStream = new BunyanLoggly(logglyConfig, config.logger.loggly.buffer);
this.extraFields = null;
}
init(job: IJob) {
public init(job: IJob) {
this.extraFields = {
jobID: job.id,
experienceName: job.experienceName
experienceName: job.experienceName,
};
}
cleanup() {
public cleanup() {
this.extraFields = null;
}
write(rec: any) {
public write(rec: any) {
const { msg: message, hostname: host, level, ...other } = rec;
this.logglyStream.write({
message,
+11 -11
View File
@@ -1,16 +1,16 @@
import bunyan from 'bunyan';
import { Client as RavenClient } from 'raven';
import { SentryStream } from 'bunyan-sentry-stream';
import bunyanDebugStream from 'bunyan-debug-stream';
import { SentryStream } from 'bunyan-sentry-stream';
import { Client as RavenClient } from 'raven';
import config from 'turtle/config';
import S3Stream from 'turtle/logger/s3Stream';
import HackyLogglyStream from 'turtle/logger/hackyLogglyStream';
import * as constants from 'turtle/constants/logger';
import { isOffline } from 'turtle/turtleContext';
import { IJob } from 'turtle/job';
import HackyLogglyStream from 'turtle/logger/hackyLogglyStream';
import S3Stream from 'turtle/logger/s3Stream';
import { isOffline } from 'turtle/turtleContext';
interface Stream {
interface IStream {
stream: any;
type?: string;
level?: string;
@@ -20,7 +20,7 @@ interface Stream {
export const s3logger = new S3Stream();
let logglyStream: HackyLogglyStream;
const streams: Stream[] = [];
const streams: IStream[] = [];
if (isOffline()) {
const prettyStdOut = new bunyanDebugStream({ forceColor: true });
@@ -33,7 +33,7 @@ if (isOffline()) {
stream: s3logger,
reemitErrorEvents: true,
level: config.logger.client.level,
}
},
);
}
@@ -56,7 +56,7 @@ if (config.logger.loggly.token) {
'app-shell-apps',
`platform.${config.platform}`,
`environment.${config.deploymentEnv}`,
]
],
};
logglyStream = new HackyLogglyStream(logglyConfig);
streams.push({
@@ -91,9 +91,9 @@ logger.cleanup = async () => {
export default logger;
export function withFields(_logger: any, extraFields: any) {
export function withFields(loggerObj: any, extraFields: any) {
return constants.LEVELS.reduce((obj, level) => {
obj[level] = (...args: any[]) => _logger[level](extraFields, ...args);
obj[level] = (...args: any[]) => loggerObj[level](extraFields, ...args);
return obj;
}, {} as any);
}
+24 -24
View File
@@ -1,6 +1,6 @@
import path from 'path';
import EventEmitter from 'events';
import path from 'path';
import fs from 'fs-extra';
import _ from 'lodash';
@@ -11,13 +11,13 @@ import * as constants from 'turtle/constants/logger';
import { IJob } from 'turtle/job';
export default class S3Stream extends EventEmitter {
s3Url: string | null;
lastFlush: number | null;
filePath: string | null;
fileHandle: number | null;
finishedPromise: Promise<any> | null;
finishedPromiseResolveFn: any;
secrets: any;
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();
@@ -29,7 +29,7 @@ export default class S3Stream extends EventEmitter {
this.finishedPromiseResolveFn = null;
}
async init(job: IJob) {
public async init(job: IJob) {
this.s3Url = `logs/${job.experienceName}/${job.id}`;
const dir = config.directories.tempS3LogsDir;
const exists = await fs.pathExists(dir);
@@ -39,12 +39,12 @@ export default class S3Stream extends EventEmitter {
this.filePath = path.join(dir, job.id);
this.fileHandle = await fs.open(this.filePath, 'w+', 0o660);
this.secrets = {};
this.finishedPromise = new Promise(res => (this.finishedPromiseResolveFn = res));
this.finishedPromise = new Promise((resolveFn) => (this.finishedPromiseResolveFn = resolveFn));
const res = await this.flush() as any;
return res.Location;
}
async cleanup() {
public async cleanup() {
const res = this.finishedPromiseResolveFn;
try {
await this.flush(true);
@@ -59,7 +59,7 @@ export default class S3Stream extends EventEmitter {
}
}
flush(force = false) {
public flush(force = false) {
if (force || !this.lastFlush) {
return this._flush();
}
@@ -69,15 +69,7 @@ export default class S3Stream extends EventEmitter {
}
}
_flush() {
this.lastFlush = Date.now();
return uploadFile({
srcPath: this.filePath as string,
key: this.s3Url as string,
});
}
write(recRaw: any) {
public write(recRaw: any) {
if (this.fileHandle) {
const rec = JSON.stringify({
...recRaw,
@@ -88,7 +80,7 @@ export default class S3Stream extends EventEmitter {
}
const recSanitized = Object.keys(this.secrets).reduce(
(acc, secret) => acc.replace(new RegExp(secret, 'g'), this.secrets[secret]),
rec
rec,
);
fs
.write(this.fileHandle as number, `${recSanitized}\n`)
@@ -98,14 +90,22 @@ export default class S3Stream extends EventEmitter {
}
}
addSecretsToFilter(secrets: any) {
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);
}
waitForLogger() {
public waitForLogger() {
return this.finishedPromise;
}
private _flush() {
this.lastFlush = Date.now();
return uploadFile({
srcPath: this.filePath as string,
key: this.s3Url as string,
});
}
}
+1 -1
View File
@@ -1,6 +1,6 @@
import logger from 'turtle/logger';
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';
+3 -3
View File
@@ -1,8 +1,8 @@
import * as cloudwatch from 'turtle/aws/cloudwatch';
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';
import * as cloudwatch from 'turtle/aws/cloudwatch';
import * as reducers from 'turtle/aws/cloudwatch/reducers';
import { TYPE_DIMENSIONS } from 'turtle/aws/cloudwatch/dimensions';
const BUILD_METRIC_NAME = 'build';
+3 -3
View File
@@ -1,12 +1,12 @@
import logger from 'turtle/logger';
import * as cloudwatch from 'turtle/aws/cloudwatch';
import * as buildStatus from 'turtle/metrics/buildStatus';
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() {
logger.info('Initializing Cloudwatch metrics');
await cloudwatch.init();
metricsToRegister.forEach(metric => metric.register());
metricsToRegister.forEach((metric) => metric.register());
}
+5 -5
View File
@@ -1,13 +1,13 @@
export = BunyanLoggly
export = BunyanLoggly;
declare class BunyanLoggly {
constructor(options: BunyanLoggly.IOptions, bufferLength?: number, bufferTimeout?: number, logglyCallback?: Function);
write(data: any): void;
constructor(options: BunyanLoggly.IOptions, bufferLength?: number, bufferTimeout?: number, logglyCallback?: any);
public write(data: any): void;
}
declare namespace BunyanLoggly {
interface IOptions {
token: string
subdomain: string
token: string;
subdomain: string;
}
}
+7 -7
View File
@@ -1,12 +1,12 @@
import { LoggerDetach } from 'xdl';
import config from 'turtle/config';
import { doJob } from 'turtle/jobManager';
import logger from 'turtle/logger';
import setup from 'turtle/setup';
import { doJob } from 'turtle/jobManager';
import { setShouldExit, checkShouldExit } from 'turtle/turtleContext';
import { checkShouldExit, setShouldExit } from 'turtle/turtleContext';
process.on('unhandledRejection', err => logger.error('Unhandled promise rejection:', err));
process.on('unhandledRejection', (err) => logger.error('Unhandled promise rejection:', err));
function handleExit() {
if (checkShouldExit()) {
@@ -14,7 +14,7 @@ function handleExit() {
process.exit(1);
}
logger.warn(
`Received termination signal. Will exit after current build. To force exit press Ctrl-C again`
`Received termination signal. Will exit after current build. To force exit press Ctrl-C again`,
);
setShouldExit();
}
@@ -23,13 +23,13 @@ process.on('SIGINT', handleExit);
async function main() {
logger.info(
`Starting Turtle... (NODE_ENV=${config.env}, PLATFORM=${config.platform}, DEPLOYMENT_ENVIRONMENT=${config.deploymentEnv})`
'Starting Turtle... '
+ `NODE_ENV=${config.env}, PLATFORM=${config.platform}, DEPLOYMENT_ENVIRONMENT=${config.deploymentEnv}`,
);
LoggerDetach.configure(logger);
if (setup[config.platform]) {
await setup[config.platform]();
}
// eslint-disable-next-line no-constant-condition
while (true) {
try {
await doJob();
@@ -41,5 +41,5 @@ async function main() {
main()
.then(() => logger.error('This should never happen...'))
.catch(err => logger.error('Something went terribly wrong :(', err))
.catch((err) => logger.error('Something went terribly wrong :(', err))
.then(() => process.exit(1));
+1 -1
View File
@@ -1,6 +1,6 @@
import { PLATFORMS } from 'turtle/constants';
import ios from 'turtle/setup/ios';
import android from 'turtle/setup/android';
import ios from 'turtle/setup/ios';
export default {
[PLATFORMS.IOS]: ios,
+3 -3
View File
@@ -1,6 +1,6 @@
import fs from 'fs-extra';
import os from 'os';
import path from 'path';
import fs from 'fs-extra';
import * as keychain from 'turtle/builders/utils/ios/keychain';
import logger from 'turtle/logger';
@@ -11,9 +11,9 @@ async function deleteProvisioningProfilesFromHomedir() {
const exists = await fs.pathExists(provisioningProfilesDir);
if (exists) {
const provisioningProfiles = (await fs.readdir(provisioningProfilesDir)).filter(
filename => path.extname(filename) === '.mobileprovision',
(filename) => path.extname(filename) === '.mobileprovision',
);
await Promise.all(provisioningProfiles.map(file => fs.remove(path.join(provisioningProfilesDir, file))));
await Promise.all(provisioningProfiles.map((file) => fs.remove(path.join(provisioningProfilesDir, file))));
}
}
+3 -3
View File
@@ -1,7 +1,7 @@
import config from 'turtle/config';
let shouldExit = false;
let _currentJobId: string | null = null;
let currentJobId: string | null = null;
export const setShouldExit = () => {
shouldExit = true;
@@ -12,11 +12,11 @@ export const checkShouldExit = () => {
};
export const getCurrentJobId = () => {
return _currentJobId;
return currentJobId;
};
export const setCurrentJobId = (id: string | null) => {
_currentJobId = id;
currentJobId = id;
};
export const isOffline = () => config.builder.mode === 'offline';
+4 -3
View File
@@ -1,7 +1,8 @@
import config from 'turtle/config';
import os from 'os';
import logger from 'turtle/logger';
import Joi from 'joi';
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';
+8 -9
View File
@@ -1,21 +1,20 @@
import Redis from 'ioredis';
import logger from 'turtle/logger';
import config from 'turtle/config';
import logger from 'turtle/logger';
const MILLIS_TO_UPLOAD_LOGS = 3000;
export const MILLIS_CONNECTION_TIMEOUT = 10000;
export enum RedisClient {
Subscriber = 'REDIS_CLIENT_SUBSCRIBER',
Default = 'REDIS_CLIENT_DEFAULT',
Configuration = 'REDIS_CLIENT_CONFIGURATION',
}
type ClientConfig = { [key: string]: string; };
interface IClientConfig { [key: string]: string; }
const clientToUrl: ClientConfig = {
const clientToUrl: IClientConfig = {
[RedisClient.Subscriber]: config.redis.url,
[RedisClient.Default]: config.redis.url,
[RedisClient.Configuration]: config.redis.configUrl,
@@ -42,21 +41,21 @@ interface IRedisClients {
[key: string]: any;
}
const _redisClients: IRedisClients = {
const redisClients: IRedisClients = {
[RedisClient.Subscriber]: null,
[RedisClient.Default]: null,
[RedisClient.Configuration]: null,
};
export async function getRedisClient(type = RedisClient.Default) {
if (!_redisClients[type]) {
if (!redisClients[type]) {
try {
_redisClients[type] = await connect(MILLIS_CONNECTION_TIMEOUT, type);
redisClients[type] = await connect(MILLIS_CONNECTION_TIMEOUT, type);
} catch (err) {
logger.error(err);
}
}
return _redisClients[type];
return redisClients[type];
}
export async function checkIfCancelled(jobId: string) {
@@ -76,7 +75,7 @@ export async function registerListener(jobId: string, deleteMessage: any) {
try {
const redis = await getRedisClient(RedisClient.Subscriber);
redis.subscribe('jobs:cancelled');
redis.on('message', async function(_: any, message: any) {
redis.on('message', async (_: any, message: any) => {
if (message === jobId) {
logger.info({ lastBuildLog: true }, 'Job cancelled - killing process');
await deleteMessage();
+2 -1
View File
@@ -4,7 +4,8 @@
"jsRules": {},
"rules": {
"quotemark": [true, "single", "jsx-double"],
"object-literal-sort-keys": false
"object-literal-sort-keys": false,
"no-namespace": false
},
"rulesDirectory": []
}
+27 -608
View File
File diff suppressed because it is too large. Load diff