mirror of
https://github.com/balena-io/balena-cli.git
synced 2025-04-21 09:30:56 +00:00
Merge pull request #1837 from balena-io/1045-deploy-typescript
Convert selected functions to Typescript and async/await (compose.js)
This commit is contained in:
commit
6a019af25f
@ -40,7 +40,10 @@ const deployProject = function(docker, logger, composeOpts, opts) {
|
||||
const _ = require('lodash');
|
||||
const doodles = require('resin-doodles');
|
||||
const sdk = getBalenaSdk();
|
||||
const { loadProject } = require('../utils/compose_ts');
|
||||
const {
|
||||
deployProject: $deployProject,
|
||||
loadProject,
|
||||
} = require('../utils/compose_ts');
|
||||
|
||||
return Promise.resolve(loadProject(logger, composeOpts, opts.image))
|
||||
.then(function(project) {
|
||||
@ -147,7 +150,7 @@ const deployProject = function(docker, logger, composeOpts, opts) {
|
||||
sdk.auth.getToken(),
|
||||
sdk.settings.get('apiUrl'),
|
||||
(userId, auth, apiEndpoint) =>
|
||||
compose.deployProject(
|
||||
$deployProject(
|
||||
docker,
|
||||
logger,
|
||||
project.composition,
|
||||
|
@ -16,6 +16,7 @@
|
||||
*/
|
||||
|
||||
import * as packageJSON from '../package.json';
|
||||
import { onceAsync } from './utils/lazy';
|
||||
|
||||
class CliSettings {
|
||||
public readonly settings: any;
|
||||
@ -46,7 +47,7 @@ class CliSettings {
|
||||
* Sentry.io setup
|
||||
* @see https://docs.sentry.io/error-reporting/quickstart/?platform=node
|
||||
*/
|
||||
async function setupSentry() {
|
||||
export const setupSentry = onceAsync(async () => {
|
||||
const config = await import('./config');
|
||||
const Sentry = await import('@sentry/node');
|
||||
Sentry.init({
|
||||
@ -60,7 +61,8 @@ async function setupSentry() {
|
||||
platform: process.platform,
|
||||
});
|
||||
});
|
||||
}
|
||||
return Sentry.getCurrentHub();
|
||||
});
|
||||
|
||||
function checkNodeVersion() {
|
||||
const validNodeVersions = packageJSON.engines.node;
|
||||
|
27
lib/utils/compose-types.d.ts
vendored
27
lib/utils/compose-types.d.ts
vendored
@ -25,6 +25,27 @@ interface Image {
|
||||
tag: string;
|
||||
}
|
||||
|
||||
export interface BuiltImage {
|
||||
logs: string;
|
||||
name: string;
|
||||
props: {
|
||||
dockerfile?: string;
|
||||
projectType?: string;
|
||||
size?: number;
|
||||
};
|
||||
serviceName: string;
|
||||
}
|
||||
|
||||
export interface TaggedImage {
|
||||
localImage: import('dockerode').Image;
|
||||
serviceImage: import('balena-release/build/models').ImageModel;
|
||||
serviceName: string;
|
||||
logs: string;
|
||||
props: BuiltImage.props;
|
||||
registry: string;
|
||||
repo: string;
|
||||
}
|
||||
|
||||
export interface ComposeOpts {
|
||||
dockerfilePath?: string;
|
||||
inlineLogs?: boolean;
|
||||
@ -40,6 +61,12 @@ export interface ComposeProject {
|
||||
descriptors: ImageDescriptor[];
|
||||
}
|
||||
|
||||
export interface Release {
|
||||
client: import('pinejs-client').ApiClient;
|
||||
release: Partial<import('balena-release/build/models').ReleaseModel>;
|
||||
serviceImages: Partial<import('balena-release/build/models').ImageModel>;
|
||||
}
|
||||
|
||||
interface TarDirectoryOptions {
|
||||
convertEol?: boolean;
|
||||
preFinalizeCallback?: (pack: Pack) => void | Promise<void>;
|
||||
|
@ -19,7 +19,7 @@ import * as Promise from 'bluebird';
|
||||
import { stripIndent } from 'common-tags';
|
||||
import * as path from 'path';
|
||||
|
||||
import { getBalenaSdk, getChalk } from './lazy';
|
||||
import { getChalk } from './lazy';
|
||||
import { IgnoreFileType } from './ignore';
|
||||
|
||||
export const appendProjectOptions = opts =>
|
||||
@ -245,6 +245,11 @@ function originalTarDirectory(dir, param) {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} str
|
||||
* @param {number} len
|
||||
* @returns {string}
|
||||
*/
|
||||
const truncateString = function(str, len) {
|
||||
if (str.length < len) {
|
||||
return str;
|
||||
@ -485,7 +490,21 @@ export function buildProject(
|
||||
.finally(renderer.end);
|
||||
}
|
||||
|
||||
const createRelease = function(apiEndpoint, auth, userId, appId, composition) {
|
||||
/**
|
||||
* @param {string} apiEndpoint
|
||||
* @param {string} auth
|
||||
* @param {number} userId
|
||||
* @param {number} appId
|
||||
* @param {import('resin-compose-parse').Composition} composition
|
||||
* @returns {Promise<import('./compose-types').Release>}
|
||||
*/
|
||||
export const createRelease = function(
|
||||
apiEndpoint,
|
||||
auth,
|
||||
userId,
|
||||
appId,
|
||||
composition,
|
||||
) {
|
||||
const _ = require('lodash');
|
||||
const crypto = require('crypto');
|
||||
const releaseMod = require('balena-release');
|
||||
@ -524,7 +543,14 @@ const createRelease = function(apiEndpoint, auth, userId, appId, composition) {
|
||||
});
|
||||
};
|
||||
|
||||
const tagServiceImages = (docker, images, serviceImages) =>
|
||||
/**
|
||||
*
|
||||
* @param {import('docker-toolbelt')} docker
|
||||
* @param {Array<import('./compose-types').BuiltImage>} images
|
||||
* @param {Partial<import('balena-release/build/models').ImageModel>} serviceImages
|
||||
* @returns {Promise<Array<import('./compose-types').TaggedImage>>}
|
||||
*/
|
||||
export const tagServiceImages = (docker, images, serviceImages) =>
|
||||
Promise.map(images, function(d) {
|
||||
const serviceImage = serviceImages[d.serviceName];
|
||||
const imageName = serviceImage.is_stored_at__image_location;
|
||||
@ -549,7 +575,14 @@ const tagServiceImages = (docker, images, serviceImages) =>
|
||||
}));
|
||||
});
|
||||
|
||||
const getPreviousRepos = (sdk, docker, logger, appID) =>
|
||||
/**
|
||||
* @param {*} sdk
|
||||
* @param {import('docker-toolbelt')} docker
|
||||
* @param {import('./logger')} logger
|
||||
* @param {number} appID
|
||||
* @returns {Promise<string[]>}
|
||||
*/
|
||||
export const getPreviousRepos = (sdk, docker, logger, appID) =>
|
||||
sdk.pine
|
||||
.get({
|
||||
resource: 'release',
|
||||
@ -590,7 +623,15 @@ const getPreviousRepos = (sdk, docker, logger, appID) =>
|
||||
return [];
|
||||
});
|
||||
|
||||
const authorizePush = function(
|
||||
/**
|
||||
* @param {*} sdk
|
||||
* @param {string} tokenAuthEndpoint
|
||||
* @param {string} registry
|
||||
* @param {string[]} images
|
||||
* @param {string[]} previousRepos
|
||||
* @returns {Promise<string>}
|
||||
*/
|
||||
export const authorizePush = function(
|
||||
sdk,
|
||||
tokenAuthEndpoint,
|
||||
registry,
|
||||
@ -613,10 +654,21 @@ const authorizePush = function(
|
||||
})
|
||||
.get('body')
|
||||
.get('token')
|
||||
.catchReturn({});
|
||||
.catchReturn('');
|
||||
};
|
||||
|
||||
const pushAndUpdateServiceImages = function(docker, token, images, afterEach) {
|
||||
/**
|
||||
* @param {import('docker-toolbelt')} docker
|
||||
* @param {string} token
|
||||
* @param {Array<import('./compose-types').TaggedImage>} images
|
||||
* @param {(serviceImage: import('balena-release/build/models').ImageModel, props: object) => void} afterEach
|
||||
*/
|
||||
export const pushAndUpdateServiceImages = function(
|
||||
docker,
|
||||
token,
|
||||
images,
|
||||
afterEach,
|
||||
) {
|
||||
const { DockerProgress } = require('docker-progress');
|
||||
const { retry } = require('./helpers');
|
||||
const tty = require('./tty')(process.stdout);
|
||||
@ -633,14 +685,18 @@ const pushAndUpdateServiceImages = function(docker, token, images, afterEach) {
|
||||
return Promise.using(tty.cursorHidden(), () =>
|
||||
Promise.map(images, ({ serviceImage, localImage, props, logs }, index) =>
|
||||
Promise.join(
|
||||
// @ts-ignore
|
||||
localImage.inspect().get('Size'),
|
||||
retry(
|
||||
// @ts-ignore
|
||||
() => progress.push(localImage.name, reporters[index], opts),
|
||||
3, // `times` - retry 3 times
|
||||
// @ts-ignore
|
||||
localImage.name, // `label` included in retry log messages
|
||||
2000, // `delayMs` - wait 2 seconds before the 1st retry
|
||||
1.4, // `backoffScaler` - wait multiplier for each retry
|
||||
).finally(renderer.end),
|
||||
/** @type {(size: number, digest: string) => void} */
|
||||
function(size, digest) {
|
||||
serviceImage.image_size = size;
|
||||
serviceImage.content_hash = digest;
|
||||
@ -666,91 +722,6 @@ const pushAndUpdateServiceImages = function(docker, token, images, afterEach) {
|
||||
);
|
||||
};
|
||||
|
||||
export function deployProject(
|
||||
docker,
|
||||
logger,
|
||||
composition,
|
||||
images,
|
||||
appId,
|
||||
userId,
|
||||
auth,
|
||||
apiEndpoint,
|
||||
skipLogUpload,
|
||||
) {
|
||||
const _ = require('lodash');
|
||||
const releaseMod = require('balena-release');
|
||||
const tty = require('./tty')(process.stdout);
|
||||
|
||||
const prefix = getChalk().cyan('[Info]') + ' ';
|
||||
const spinner = createSpinner();
|
||||
let runloop = runSpinner(tty, spinner, `${prefix}Creating release...`);
|
||||
|
||||
return createRelease(apiEndpoint, auth, userId, appId, composition)
|
||||
.finally(runloop.end)
|
||||
.then(function({ client, release, serviceImages }) {
|
||||
logger.logDebug('Tagging images...');
|
||||
return tagServiceImages(docker, images, serviceImages)
|
||||
.tap(function(taggedImages) {
|
||||
logger.logDebug('Authorizing push...');
|
||||
const sdk = getBalenaSdk();
|
||||
return getPreviousRepos(sdk, docker, logger, appId)
|
||||
.then(previousRepos =>
|
||||
authorizePush(
|
||||
sdk,
|
||||
apiEndpoint,
|
||||
taggedImages[0].registry,
|
||||
_.map(taggedImages, 'repo'),
|
||||
previousRepos,
|
||||
),
|
||||
)
|
||||
.then(function(token) {
|
||||
logger.logInfo('Pushing images to registry...');
|
||||
return pushAndUpdateServiceImages(
|
||||
docker,
|
||||
token,
|
||||
taggedImages,
|
||||
function(serviceImage) {
|
||||
logger.logDebug(
|
||||
`Saving image ${serviceImage.is_stored_at__image_location}`,
|
||||
);
|
||||
if (skipLogUpload) {
|
||||
delete serviceImage.build_log;
|
||||
}
|
||||
return releaseMod.updateImage(
|
||||
client,
|
||||
serviceImage.id,
|
||||
serviceImage,
|
||||
);
|
||||
},
|
||||
);
|
||||
})
|
||||
.finally(function() {
|
||||
logger.logDebug('Untagging images...');
|
||||
return Promise.map(taggedImages, ({ localImage }) =>
|
||||
localImage.remove(),
|
||||
);
|
||||
});
|
||||
})
|
||||
.then(() => {
|
||||
release.status = 'success';
|
||||
})
|
||||
.tapCatch(() => {
|
||||
release.status = 'failed';
|
||||
})
|
||||
.finally(function() {
|
||||
runloop = runSpinner(tty, spinner, `${prefix}Saving release...`);
|
||||
release.end_timestamp = new Date();
|
||||
if (release.id == null) {
|
||||
return;
|
||||
}
|
||||
return releaseMod
|
||||
.updateRelease(client, release.id, release)
|
||||
.finally(runloop.end);
|
||||
})
|
||||
.return(release);
|
||||
});
|
||||
}
|
||||
|
||||
// utilities
|
||||
|
||||
const renderProgressBar = function(percentage, stepCount) {
|
||||
@ -874,39 +845,6 @@ var pullProgressAdapter = outStream =>
|
||||
});
|
||||
};
|
||||
|
||||
var createSpinner = function() {
|
||||
const chars = '|/-\\';
|
||||
let index = 0;
|
||||
return () => chars[index++ % chars.length];
|
||||
};
|
||||
|
||||
var runSpinner = function(tty, spinner, msg) {
|
||||
const runloop = createRunLoop(function() {
|
||||
tty.clearLine();
|
||||
tty.writeLine(`${msg} ${spinner()}`);
|
||||
return tty.cursorUp();
|
||||
});
|
||||
runloop.onEnd = function() {
|
||||
tty.clearLine();
|
||||
return tty.writeLine(msg);
|
||||
};
|
||||
return runloop;
|
||||
};
|
||||
|
||||
var createRunLoop = function(tick) {
|
||||
const timerId = setInterval(tick, 1000 / 10);
|
||||
var runloop = {
|
||||
onEnd() {
|
||||
// noop
|
||||
},
|
||||
end() {
|
||||
clearInterval(timerId);
|
||||
return runloop.onEnd();
|
||||
},
|
||||
};
|
||||
return runloop;
|
||||
};
|
||||
|
||||
class BuildProgressUI {
|
||||
constructor(tty, descriptors) {
|
||||
this._handleEvent = this._handleEvent.bind(this);
|
||||
@ -953,7 +891,7 @@ class BuildProgressUI {
|
||||
this._startTime = null;
|
||||
this._ended = false;
|
||||
this._cancelled = false;
|
||||
this._spinner = createSpinner();
|
||||
this._spinner = require('./compose_ts').createSpinner();
|
||||
|
||||
this.streams = streams;
|
||||
}
|
||||
@ -974,7 +912,7 @@ class BuildProgressUI {
|
||||
this._services.forEach(service => {
|
||||
this.streams[service].write({ status: 'Preparing...' });
|
||||
});
|
||||
this._runloop = createRunLoop(this._display);
|
||||
this._runloop = require('./compose_ts').createRunLoop(this._display);
|
||||
this._startTime = Date.now();
|
||||
}
|
||||
|
||||
|
@ -27,6 +27,15 @@ import { Readable } from 'stream';
|
||||
import * as tar from 'tar-stream';
|
||||
|
||||
import { ExpectedError } from '../errors';
|
||||
import { getBalenaSdk, getChalk } from '../utils/lazy';
|
||||
import {
|
||||
BuiltImage,
|
||||
ComposeOpts,
|
||||
ComposeProject,
|
||||
Release,
|
||||
TaggedImage,
|
||||
TarDirectoryOptions,
|
||||
} from './compose-types';
|
||||
import { DeviceInfo } from './device/api';
|
||||
import Logger = require('./logger');
|
||||
|
||||
@ -47,9 +56,9 @@ const compositionFileNames = ['docker-compose.yml', 'docker-compose.yaml'];
|
||||
*/
|
||||
export async function loadProject(
|
||||
logger: Logger,
|
||||
opts: import('./compose-types').ComposeOpts,
|
||||
opts: ComposeOpts,
|
||||
image?: string,
|
||||
): Promise<import('./compose-types').ComposeProject> {
|
||||
): Promise<ComposeProject> {
|
||||
const compose = await import('resin-compose-parse');
|
||||
const { createProject } = await import('./compose');
|
||||
let composeName: string;
|
||||
@ -170,7 +179,7 @@ export async function tarDirectory(
|
||||
preFinalizeCallback,
|
||||
convertEol = false,
|
||||
nogitignore = false,
|
||||
}: import('./compose-types').TarDirectoryOptions,
|
||||
}: TarDirectoryOptions,
|
||||
): Promise<import('stream').Readable> {
|
||||
(await import('assert')).strict.strictEqual(nogitignore, true);
|
||||
const { filterFilesWithDockerignore } = await import('./ignore');
|
||||
@ -567,3 +576,162 @@ export async function validateProjectDirectory(
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
async function getTokenForPreviousRepos(
|
||||
docker: import('docker-toolbelt'),
|
||||
logger: Logger,
|
||||
appId: number,
|
||||
apiEndpoint: string,
|
||||
taggedImages: TaggedImage[],
|
||||
): Promise<string> {
|
||||
logger.logDebug('Authorizing push...');
|
||||
const { authorizePush, getPreviousRepos } = await import('./compose');
|
||||
const sdk = getBalenaSdk();
|
||||
const previousRepos = await getPreviousRepos(sdk, docker, logger, appId);
|
||||
if (!previousRepos || previousRepos.length === 0) {
|
||||
return '';
|
||||
}
|
||||
const token = await authorizePush(
|
||||
sdk,
|
||||
apiEndpoint,
|
||||
taggedImages[0].registry,
|
||||
_.map(taggedImages, 'repo'),
|
||||
previousRepos,
|
||||
);
|
||||
return token;
|
||||
}
|
||||
|
||||
async function pushServiceImages(
|
||||
docker: import('docker-toolbelt'),
|
||||
logger: Logger,
|
||||
pineClient: import('pinejs-client'),
|
||||
taggedImages: TaggedImage[],
|
||||
token: string,
|
||||
skipLogUpload: boolean,
|
||||
): Promise<void> {
|
||||
const { pushAndUpdateServiceImages } = await import('./compose');
|
||||
const releaseMod = await import('balena-release');
|
||||
logger.logInfo('Pushing images to registry...');
|
||||
await pushAndUpdateServiceImages(docker, token, taggedImages, async function(
|
||||
serviceImage,
|
||||
) {
|
||||
logger.logDebug(
|
||||
`Saving image ${serviceImage.is_stored_at__image_location}`,
|
||||
);
|
||||
if (skipLogUpload) {
|
||||
delete serviceImage.build_log;
|
||||
}
|
||||
await releaseMod.updateImage(pineClient, serviceImage.id, serviceImage);
|
||||
});
|
||||
}
|
||||
|
||||
export async function deployProject(
|
||||
docker: import('docker-toolbelt'),
|
||||
logger: Logger,
|
||||
composition: import('resin-compose-parse').Composition,
|
||||
images: BuiltImage[],
|
||||
appId: number,
|
||||
userId: number,
|
||||
auth: string,
|
||||
apiEndpoint: string,
|
||||
skipLogUpload: boolean,
|
||||
): Promise<Partial<import('balena-release/build/models').ReleaseModel>> {
|
||||
const releaseMod = require('balena-release');
|
||||
const { createRelease, tagServiceImages } = await import('./compose');
|
||||
const tty = (await import('./tty'))(process.stdout);
|
||||
|
||||
const prefix = getChalk().cyan('[Info]') + ' ';
|
||||
const spinner = createSpinner();
|
||||
let runloop = runSpinner(tty, spinner, `${prefix}Creating release...`);
|
||||
|
||||
let $release: Release;
|
||||
try {
|
||||
$release = await createRelease(
|
||||
apiEndpoint,
|
||||
auth,
|
||||
userId,
|
||||
appId,
|
||||
composition,
|
||||
);
|
||||
} finally {
|
||||
runloop.end();
|
||||
}
|
||||
const { client: pineClient, release, serviceImages } = $release;
|
||||
|
||||
try {
|
||||
logger.logDebug('Tagging images...');
|
||||
const taggedImages = await tagServiceImages(docker, images, serviceImages);
|
||||
try {
|
||||
const token = await getTokenForPreviousRepos(
|
||||
docker,
|
||||
logger,
|
||||
appId,
|
||||
apiEndpoint,
|
||||
taggedImages,
|
||||
);
|
||||
await pushServiceImages(
|
||||
docker,
|
||||
logger,
|
||||
pineClient,
|
||||
taggedImages,
|
||||
token,
|
||||
skipLogUpload,
|
||||
);
|
||||
release.status = 'success';
|
||||
} catch (err) {
|
||||
release.status = 'failed';
|
||||
throw err;
|
||||
} finally {
|
||||
logger.logDebug('Untagging images...');
|
||||
await Bluebird.map(taggedImages, ({ localImage }) => localImage.remove());
|
||||
}
|
||||
} finally {
|
||||
runloop = runSpinner(tty, spinner, `${prefix}Saving release...`);
|
||||
release.end_timestamp = new Date();
|
||||
if (release.id != null) {
|
||||
try {
|
||||
await releaseMod.updateRelease(pineClient, release.id, release);
|
||||
} finally {
|
||||
runloop.end();
|
||||
}
|
||||
}
|
||||
}
|
||||
return release;
|
||||
}
|
||||
|
||||
export function createSpinner() {
|
||||
const chars = '|/-\\';
|
||||
let index = 0;
|
||||
return () => chars[index++ % chars.length];
|
||||
}
|
||||
|
||||
function runSpinner(
|
||||
tty: ReturnType<typeof import('./tty')>,
|
||||
spinner: () => string,
|
||||
msg: string,
|
||||
) {
|
||||
const runloop = createRunLoop(function() {
|
||||
tty.clearLine();
|
||||
tty.writeLine(`${msg} ${spinner()}`);
|
||||
return tty.cursorUp();
|
||||
});
|
||||
runloop.onEnd = function() {
|
||||
tty.clearLine();
|
||||
return tty.writeLine(msg);
|
||||
};
|
||||
return runloop;
|
||||
}
|
||||
|
||||
export function createRunLoop(tick: (...args: any[]) => void) {
|
||||
const timerId = setInterval(tick, 1000 / 10);
|
||||
const runloop = {
|
||||
onEnd() {
|
||||
// noop
|
||||
},
|
||||
end() {
|
||||
clearInterval(timerId);
|
||||
return runloop.onEnd();
|
||||
},
|
||||
};
|
||||
return runloop;
|
||||
}
|
||||
|
@ -31,6 +31,16 @@ const once = <T>(fn: () => T) => {
|
||||
};
|
||||
};
|
||||
|
||||
export const onceAsync = <T>(fn: () => Promise<T>) => {
|
||||
let cached: T;
|
||||
return async (): Promise<T> => {
|
||||
if (!cached) {
|
||||
cached = await fn();
|
||||
}
|
||||
return cached;
|
||||
};
|
||||
};
|
||||
|
||||
export const getBalenaSdk = once(() =>
|
||||
(require('balena-sdk') as typeof BalenaSdk).fromSharedOptions(),
|
||||
);
|
||||
|
135
npm-shrinkwrap.json
generated
135
npm-shrinkwrap.json
generated
@ -814,33 +814,42 @@
|
||||
"integrity": "sha512-9NET910DNaIPngYnLLPeg+Ogzqsi9uM4mSboU5y6p8S5DzMTVEsJZrawi+BoDNUVBa2DhJqQYUFvMDfgU062LQ=="
|
||||
},
|
||||
"@sinonjs/commons": {
|
||||
"version": "1.7.0",
|
||||
"resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.7.0.tgz",
|
||||
"integrity": "sha512-qbk9AP+cZUsKdW1GJsBpxPKFmCJ0T8swwzVje3qFd+AkQb74Q/tiuzrdfFg8AD2g5HH/XbE/I8Uc1KYHVYWfhg==",
|
||||
"version": "1.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.0.tgz",
|
||||
"integrity": "sha512-wEj54PfsZ5jGSwMX68G8ZXFawcSglQSXqCftWX3ec8MDUzQdHgcKvw97awHbY0efQEL5iKUOAmmVtoYgmrSG4Q==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"type-detect": "4.0.8"
|
||||
}
|
||||
},
|
||||
"@sinonjs/fake-timers": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-6.0.1.tgz",
|
||||
"integrity": "sha512-MZPUxrmFubI36XS1DI3qmI0YdN1gks62JtFZvxR67ljjSNCeK6U08Zx4msEWOXuofgqUt6zPHSi1H9fbjR/NRA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@sinonjs/commons": "^1.7.0"
|
||||
}
|
||||
},
|
||||
"@sinonjs/formatio": {
|
||||
"version": "3.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@sinonjs/formatio/-/formatio-3.2.2.tgz",
|
||||
"integrity": "sha512-B8SEsgd8gArBLMD6zpRw3juQ2FVSsmdd7qlevyDqzS9WTCtvF55/gAL+h6gue8ZvPYcdiPdvueM/qm//9XzyTQ==",
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@sinonjs/formatio/-/formatio-5.0.1.tgz",
|
||||
"integrity": "sha512-KaiQ5pBf1MpS09MuA0kp6KBQt2JUOQycqVG1NZXvzeaXe5LGFqAKueIS0bw4w0P9r7KuBSVdUk5QjXsUdu2CxQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@sinonjs/commons": "^1",
|
||||
"@sinonjs/samsam": "^3.1.0"
|
||||
"@sinonjs/samsam": "^5.0.2"
|
||||
}
|
||||
},
|
||||
"@sinonjs/samsam": {
|
||||
"version": "3.3.3",
|
||||
"resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-3.3.3.tgz",
|
||||
"integrity": "sha512-bKCMKZvWIjYD0BLGnNrxVuw4dkWCYsLqFOUWw8VgKF/+5Y+mE7LfHWPIYoDXowH+3a9LsWDMo0uAP8YDosPvHQ==",
|
||||
"version": "5.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-5.0.3.tgz",
|
||||
"integrity": "sha512-QucHkc2uMJ0pFGjJUDP3F9dq5dx8QIaqISl9QgwLOh6P9yv877uONPGXh/OH/0zmM3tW1JjuJltAZV2l7zU+uQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@sinonjs/commons": "^1.3.0",
|
||||
"array-from": "^2.1.1",
|
||||
"lodash": "^4.17.15"
|
||||
"@sinonjs/commons": "^1.6.0",
|
||||
"lodash.get": "^4.4.2",
|
||||
"type-detect": "^4.0.8"
|
||||
}
|
||||
},
|
||||
"@sinonjs/text-encoding": {
|
||||
@ -1295,9 +1304,18 @@
|
||||
"dev": true
|
||||
},
|
||||
"@types/sinon": {
|
||||
"version": "7.5.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/sinon/-/sinon-7.5.2.tgz",
|
||||
"integrity": "sha512-T+m89VdXj/eidZyejvmoP9jivXgBDdkOSBVQjU9kF349NEx10QdPNGxHeZUaj1IlJ32/ewdyXJjnJxyxJroYwg==",
|
||||
"version": "9.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/sinon/-/sinon-9.0.3.tgz",
|
||||
"integrity": "sha512-NWVG++603tEDwmz5k0DwFR1hqP3iBmq5GYi6d+0KCQMQsfDEULF1D7xqZ+iXRJHeGwLVhM+Rv73uzIYuIUVlJQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/sinonjs__fake-timers": "*"
|
||||
}
|
||||
},
|
||||
"@types/sinonjs__fake-timers": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-6.0.1.tgz",
|
||||
"integrity": "sha512-yYezQwGWty8ziyYLdZjwxyMb0CZR49h8JALHGrxjQHWlqGgc8kLdHEgWrgL0uZ29DMvEVBDnHU2Wg36zKSIUtA==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/split": {
|
||||
@ -1733,12 +1751,6 @@
|
||||
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-2.1.2.tgz",
|
||||
"integrity": "sha512-hNfzcOV8W4NdualtqBFPyVO+54DSJuZGY9qT4pRroB6S9e3iiido2ISIC5h9R2sPJ8H3FHCIiEnsv1lPXO3KtQ=="
|
||||
},
|
||||
"array-from": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/array-from/-/array-from-2.1.1.tgz",
|
||||
"integrity": "sha1-z+nYwmYoudxa7MYqn12PHzUsEZU=",
|
||||
"dev": true
|
||||
},
|
||||
"array-initial": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/array-initial/-/array-initial-1.1.0.tgz",
|
||||
@ -8314,9 +8326,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"just-extend": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/just-extend/-/just-extend-4.0.2.tgz",
|
||||
"integrity": "sha512-FrLwOgm+iXrPV+5zDU6Jqu4gCRXbWEQg2O3SKONsWE4w7AXFRkryS53bpWdaL9cNol+AmR3AEYz6kn+o0fCPnw==",
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/just-extend/-/just-extend-4.1.0.tgz",
|
||||
"integrity": "sha512-ApcjaOdVTJ7y4r08xI5wIqpvwS48Q0PBG4DJROcEkH1f8MdAiNFyFxz3xoL0LWAVwjrwPYZdVHHxhRHcx/uGLA==",
|
||||
"dev": true
|
||||
},
|
||||
"jwt-decode": {
|
||||
@ -8756,12 +8768,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"lolex": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/lolex/-/lolex-4.2.0.tgz",
|
||||
"integrity": "sha512-gKO5uExCXvSm6zbF562EvM+rd1kQDnB9AZBbiQVzf1ZmdDpxUSvpnAaVOP83N/31mRK8Ml8/VE8DMvsAZQ+7wg==",
|
||||
"dev": true
|
||||
},
|
||||
"loose-envify": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
|
||||
@ -13127,15 +13133,15 @@
|
||||
"integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ=="
|
||||
},
|
||||
"nise": {
|
||||
"version": "1.5.3",
|
||||
"resolved": "https://registry.npmjs.org/nise/-/nise-1.5.3.tgz",
|
||||
"integrity": "sha512-Ymbac/94xeIrMf59REBPOv0thr+CJVFMhrlAkW/gjCIE58BGQdCj0x7KRCb3yz+Ga2Rz3E9XXSvUyyxqqhjQAQ==",
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/nise/-/nise-4.0.3.tgz",
|
||||
"integrity": "sha512-EGlhjm7/4KvmmE6B/UFsKh7eHykRl9VH+au8dduHLCyWUO/hr7+N+WtTvDUwc9zHuM1IaIJs/0lQ6Ag1jDkQSg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@sinonjs/formatio": "^3.2.1",
|
||||
"@sinonjs/commons": "^1.7.0",
|
||||
"@sinonjs/fake-timers": "^6.0.0",
|
||||
"@sinonjs/text-encoding": "^0.7.1",
|
||||
"just-extend": "^4.0.2",
|
||||
"lolex": "^5.0.1",
|
||||
"path-to-regexp": "^1.7.0"
|
||||
},
|
||||
"dependencies": {
|
||||
@ -13145,15 +13151,6 @@
|
||||
"integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=",
|
||||
"dev": true
|
||||
},
|
||||
"lolex": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/lolex/-/lolex-5.1.2.tgz",
|
||||
"integrity": "sha512-h4hmjAvHTmd+25JSwrtTIuwbKdwg5NzZVRMLn9saij4SZaepCrTCxPr35H/3bjwfMJtN+t3CX8672UIkglz28A==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@sinonjs/commons": "^1.7.0"
|
||||
}
|
||||
},
|
||||
"path-to-regexp": {
|
||||
"version": "1.8.0",
|
||||
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz",
|
||||
@ -13166,15 +13163,14 @@
|
||||
}
|
||||
},
|
||||
"nock": {
|
||||
"version": "11.9.1",
|
||||
"resolved": "https://registry.npmjs.org/nock/-/nock-11.9.1.tgz",
|
||||
"integrity": "sha512-U5wPctaY4/ar2JJ5Jg4wJxlbBfayxgKbiAeGh+a1kk6Pwnc2ZEuKviLyDSG6t0uXl56q7AALIxoM6FJrBSsVXA==",
|
||||
"version": "12.0.3",
|
||||
"resolved": "https://registry.npmjs.org/nock/-/nock-12.0.3.tgz",
|
||||
"integrity": "sha512-QNb/j8kbFnKCiyqi9C5DD0jH/FubFGj5rt9NQFONXwQm3IPB0CULECg/eS3AU1KgZb/6SwUa4/DTRKhVxkGABw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"debug": "^4.1.0",
|
||||
"json-stringify-safe": "^5.0.1",
|
||||
"lodash": "^4.17.13",
|
||||
"mkdirp": "^0.5.0",
|
||||
"propagate": "^2.0.0"
|
||||
}
|
||||
},
|
||||
@ -16274,25 +16270,40 @@
|
||||
"dev": true
|
||||
},
|
||||
"sinon": {
|
||||
"version": "7.5.0",
|
||||
"resolved": "https://registry.npmjs.org/sinon/-/sinon-7.5.0.tgz",
|
||||
"integrity": "sha512-AoD0oJWerp0/rY9czP/D6hDTTUYGpObhZjMpd7Cl/A6+j0xBE+ayL/ldfggkBXUs0IkvIiM1ljM8+WkOc5k78Q==",
|
||||
"version": "9.0.2",
|
||||
"resolved": "https://registry.npmjs.org/sinon/-/sinon-9.0.2.tgz",
|
||||
"integrity": "sha512-0uF8Q/QHkizNUmbK3LRFqx5cpTttEVXudywY9Uwzy8bTfZUhljZ7ARzSxnRHWYWtVTeh4Cw+tTb3iU21FQVO9A==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@sinonjs/commons": "^1.4.0",
|
||||
"@sinonjs/formatio": "^3.2.1",
|
||||
"@sinonjs/samsam": "^3.3.3",
|
||||
"diff": "^3.5.0",
|
||||
"lolex": "^4.2.0",
|
||||
"nise": "^1.5.2",
|
||||
"supports-color": "^5.5.0"
|
||||
"@sinonjs/commons": "^1.7.2",
|
||||
"@sinonjs/fake-timers": "^6.0.1",
|
||||
"@sinonjs/formatio": "^5.0.1",
|
||||
"@sinonjs/samsam": "^5.0.3",
|
||||
"diff": "^4.0.2",
|
||||
"nise": "^4.0.1",
|
||||
"supports-color": "^7.1.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"diff": {
|
||||
"version": "3.5.0",
|
||||
"resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz",
|
||||
"integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==",
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz",
|
||||
"integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==",
|
||||
"dev": true
|
||||
},
|
||||
"has-flag": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
|
||||
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
|
||||
"dev": true
|
||||
},
|
||||
"supports-color": {
|
||||
"version": "7.1.0",
|
||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz",
|
||||
"integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"has-flag": "^4.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -135,7 +135,7 @@
|
||||
"@types/rewire": "^2.5.28",
|
||||
"@types/rimraf": "^2.0.4",
|
||||
"@types/shell-escape": "^0.2.0",
|
||||
"@types/sinon": "^7.5.2",
|
||||
"@types/sinon": "^9.0.3",
|
||||
"@types/split": "^1.0.0",
|
||||
"@types/stream-to-promise": "2.2.0",
|
||||
"@types/tar-stream": "^2.1.0",
|
||||
@ -154,13 +154,13 @@
|
||||
"intercept-stdout": "^0.1.2",
|
||||
"mocha": "^6.2.3",
|
||||
"mock-require": "^3.0.3",
|
||||
"nock": "^11.9.1",
|
||||
"nock": "^12.0.3",
|
||||
"parse-link-header": "~1.0.1",
|
||||
"pkg": "^4.4.2",
|
||||
"publish-release": "^1.6.1",
|
||||
"rewire": "^4.0.1",
|
||||
"simple-git": "^1.131.0",
|
||||
"sinon": "^7.5.0",
|
||||
"sinon": "^9.0.2",
|
||||
"ts-node": "^8.10.1",
|
||||
"typescript": "^3.9.2"
|
||||
},
|
||||
|
@ -52,7 +52,6 @@ export class BalenaAPIMock extends NockMock {
|
||||
|
||||
public expectGetAuth(opts: ScopeOpts = {}) {
|
||||
this.optGet(/^\/auth\/v1\//, opts).reply(200, {
|
||||
// "token": "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6IlJZVFk6TlE3WDpKSDVCOlFFWFk6RkU2TjpLTlVVOklWNTI6TFFRQTo3UjRWOjJVUFI6Qk9ISjpDNklPIn0.eyJqdGkiOiI3ZTNlN2RmMS1iYjljLTQxZTMtOTlkMi00NjVlMjE4YzFmOWQiLCJuYmYiOjE1NzkxOTQ1MjgsImFjY2VzcyI6W3sibmFtZSI6InYyL2MwODljNDIxZmIyMzM2ZDA0NzUxNjZmYmYzZDBmOWZhIiwidHlwZSI6InJlcG9zaXRvcnkiLCJhY3Rpb25zIjpbInB1bGwiLCJwdXNoIl19LHsibmFtZSI6InYyLzljMDBjOTQxMzk0MmNkMTVjZmM5MTg5YzVkYWMzNTlkIiwidHlwZSI6InJlcG9zaXRvcnkiLCJhY3Rpb25zIjpbInB1bGwiLCJwdXNoIl19XSwiaWF0IjoxNTc5MTk0NTM4LCJleHAiOjE1NzkyMDg5MzgsImF1ZCI6InJlZ2lzdHJ5Mi5iYWxlbmEtY2xvdWQuY29tIiwiaXNzIjoiYXBpLmJhbGVuYS1jbG91ZC5jb20iLCJzdWIiOiJnaF9wYXVsb19jYXN0cm8ifQ.bRw5_lg-nT-c1V4RxIJjujfPuVewZTs0BRNENEw2-sk_6zepLs-sLl9DOSEHYBdi87EtyCiUB3Wqee6fvz2HyQ"
|
||||
token: 'test',
|
||||
});
|
||||
}
|
||||
@ -65,8 +64,17 @@ export class BalenaAPIMock extends NockMock {
|
||||
);
|
||||
}
|
||||
|
||||
public expectPatchRelease(opts: ScopeOpts = {}) {
|
||||
this.optPatch(/^\/v5\/release($|[(?])/, opts).reply(200, 'OK');
|
||||
public expectPatchRelease({
|
||||
replyBody = 'OK',
|
||||
statusCode = 200,
|
||||
inspectRequest = this.inspectNoOp,
|
||||
optional = false,
|
||||
persist = false,
|
||||
}) {
|
||||
this.optPatch(/^\/v5\/release($|[(?])/, { optional, persist }).reply(
|
||||
statusCode,
|
||||
this.getInspectedReplyBodyFunction(inspectRequest, replyBody),
|
||||
);
|
||||
}
|
||||
|
||||
public expectPostRelease(opts: ScopeOpts = {}) {
|
||||
@ -77,8 +85,17 @@ export class BalenaAPIMock extends NockMock {
|
||||
);
|
||||
}
|
||||
|
||||
public expectPatchImage(opts: ScopeOpts = {}) {
|
||||
this.optPatch(/^\/v5\/image($|[(?])/, opts).reply(200, 'OK');
|
||||
public expectPatchImage({
|
||||
replyBody = 'OK',
|
||||
statusCode = 200,
|
||||
inspectRequest = this.inspectNoOp,
|
||||
optional = false,
|
||||
persist = false,
|
||||
}) {
|
||||
this.optPatch(/^\/v5\/image($|[(?])/, { optional, persist }).reply(
|
||||
statusCode,
|
||||
this.getInspectedReplyBodyFunction(inspectRequest, replyBody),
|
||||
);
|
||||
}
|
||||
|
||||
public expectPostImage(opts: ScopeOpts = {}) {
|
||||
|
@ -21,11 +21,12 @@ require('../config-tests'); // required for side effects
|
||||
import { expect } from 'chai';
|
||||
import { fs } from 'mz';
|
||||
import * as path from 'path';
|
||||
import * as sinon from 'sinon';
|
||||
|
||||
import { BalenaAPIMock } from '../balena-api-mock';
|
||||
import { testDockerBuildStream } from '../docker-build';
|
||||
import { DockerMock, dockerResponsePath } from '../docker-mock';
|
||||
import { cleanOutput, runCommand } from '../helpers';
|
||||
import { cleanOutput, runCommand, switchSentry } from '../helpers';
|
||||
import { ExpectedTarStreamFiles } from '../projects';
|
||||
|
||||
const repoPath = path.normalize(path.join(__dirname, '..', '..'));
|
||||
@ -55,8 +56,20 @@ const commonQueryParams = [
|
||||
describe('balena deploy', function() {
|
||||
let api: BalenaAPIMock;
|
||||
let docker: DockerMock;
|
||||
let sentryStatus: boolean | undefined;
|
||||
const isWindows = process.platform === 'win32';
|
||||
|
||||
this.beforeAll(async () => {
|
||||
sentryStatus = await switchSentry(false);
|
||||
sinon.stub(process, 'exit');
|
||||
});
|
||||
|
||||
this.afterAll(async () => {
|
||||
await switchSentry(sentryStatus);
|
||||
// @ts-ignore
|
||||
process.exit.restore();
|
||||
});
|
||||
|
||||
this.beforeEach(() => {
|
||||
api = new BalenaAPIMock();
|
||||
docker = new DockerMock();
|
||||
@ -64,7 +77,6 @@ describe('balena deploy', function() {
|
||||
api.expectGetMixpanel({ optional: true });
|
||||
api.expectGetDeviceTypes();
|
||||
api.expectGetApplication();
|
||||
api.expectPatchRelease();
|
||||
api.expectPostRelease();
|
||||
api.expectGetRelease();
|
||||
api.expectGetUser();
|
||||
@ -74,7 +86,6 @@ describe('balena deploy', function() {
|
||||
api.expectPostImage();
|
||||
api.expectPostImageIsPartOfRelease();
|
||||
api.expectPostImageLabel();
|
||||
api.expectPatchImage();
|
||||
|
||||
docker.expectGetPing();
|
||||
docker.expectGetInfo({});
|
||||
@ -119,6 +130,9 @@ describe('balena deploy', function() {
|
||||
);
|
||||
}
|
||||
|
||||
api.expectPatchImage({});
|
||||
api.expectPatchRelease({});
|
||||
|
||||
await testDockerBuildStream({
|
||||
commandLine: `deploy testApp --build --source ${projectPath} -G`,
|
||||
dockerMock: docker,
|
||||
@ -131,6 +145,64 @@ describe('balena deploy', function() {
|
||||
services: ['main'],
|
||||
});
|
||||
});
|
||||
|
||||
it('should update a release with status="failed" on error (single container)', async () => {
|
||||
const projectPath = path.join(projectsPath, 'no-docker-compose', 'basic');
|
||||
const expectedFiles: ExpectedTarStreamFiles = {
|
||||
'src/start.sh': { fileSize: 89, type: 'file' },
|
||||
'src/windows-crlf.sh': { fileSize: 70, type: 'file' },
|
||||
Dockerfile: { fileSize: 88, type: 'file' },
|
||||
'Dockerfile-alt': { fileSize: 30, type: 'file' },
|
||||
};
|
||||
const responseFilename = 'build-POST.json';
|
||||
const responseBody = await fs.readFile(
|
||||
path.join(dockerResponsePath, responseFilename),
|
||||
'utf8',
|
||||
);
|
||||
const expectedResponseLines = ['[Error] Deploy failed'];
|
||||
const errMsg = 'Patch Image Error';
|
||||
const expectedErrorLines = [errMsg];
|
||||
|
||||
// Mock this patch HTTP request to return status code 500, in which case
|
||||
// the release status should be saved as "failed" rather than "success"
|
||||
api.expectPatchImage({
|
||||
replyBody: errMsg,
|
||||
statusCode: 500,
|
||||
inspectRequest: (_uri, requestBody) => {
|
||||
const imageBody = requestBody as Partial<
|
||||
import('balena-release/build/models').ImageModel
|
||||
>;
|
||||
expect(imageBody.status).to.equal('success');
|
||||
},
|
||||
});
|
||||
// Check that the CLI patches the release with status="failed"
|
||||
api.expectPatchRelease({
|
||||
inspectRequest: (_uri, requestBody) => {
|
||||
const releaseBody = requestBody as Partial<
|
||||
import('balena-release/build/models').ReleaseModel
|
||||
>;
|
||||
expect(releaseBody.status).to.equal('failed');
|
||||
},
|
||||
});
|
||||
|
||||
await testDockerBuildStream({
|
||||
commandLine: `deploy testApp --build --source ${projectPath} -G`,
|
||||
dockerMock: docker,
|
||||
expectedFilesByService: { main: expectedFiles },
|
||||
expectedQueryParamsByService: { main: commonQueryParams },
|
||||
expectedErrorLines,
|
||||
expectedResponseLines,
|
||||
projectPath,
|
||||
responseBody,
|
||||
responseCode: 200,
|
||||
services: ['main'],
|
||||
});
|
||||
// The SDK should produce an "unexpected" BalenaRequestError, which
|
||||
// causes the CLI to call process.exit() with process.exitCode = 1
|
||||
// @ts-ignore
|
||||
sinon.assert.calledWith(process.exit);
|
||||
expect(process.exitCode).to.equal(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('balena deploy: project validation', function() {
|
||||
|
@ -138,12 +138,14 @@ export async function testDockerBuildStream(o: {
|
||||
dockerMock: DockerMock;
|
||||
expectedFilesByService: ExpectedTarStreamFilesByService;
|
||||
expectedQueryParamsByService: { [service: string]: string[][] };
|
||||
expectedErrorLines?: string[];
|
||||
expectedResponseLines: string[];
|
||||
projectPath: string;
|
||||
responseCode: number;
|
||||
responseBody: string;
|
||||
services: string[]; // e.g. ['main'] or ['service1', 'service2']
|
||||
}) {
|
||||
const expectedErrorLines = fillTemplateArray(o.expectedErrorLines || [], o);
|
||||
const expectedResponseLines = fillTemplateArray(o.expectedResponseLines, o);
|
||||
|
||||
for (const service of o.services) {
|
||||
@ -174,10 +176,19 @@ export async function testDockerBuildStream(o: {
|
||||
|
||||
const { out, err } = await runCommand(o.commandLine);
|
||||
|
||||
expect(err).to.be.empty;
|
||||
expect(
|
||||
cleanOutput(out).map(line => line.replace(/\s{2,}/g, ' ')),
|
||||
).to.include.members(expectedResponseLines);
|
||||
const cleanLines = (lines: string[]) =>
|
||||
cleanOutput(lines).map(line => line.replace(/\s{2,}/g, ' '));
|
||||
|
||||
if (expectedErrorLines.length) {
|
||||
expect(cleanLines(err)).to.include.members(expectedErrorLines);
|
||||
} else {
|
||||
expect(err).to.be.empty;
|
||||
}
|
||||
if (expectedResponseLines.length) {
|
||||
expect(cleanLines(out)).to.include.members(expectedResponseLines);
|
||||
} else {
|
||||
expect(out).to.be.empty;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -24,6 +24,7 @@ import * as nock from 'nock';
|
||||
import * as path from 'path';
|
||||
|
||||
import * as balenaCLI from '../build/app';
|
||||
import { setupSentry } from '../build/app-common';
|
||||
|
||||
export const runCommand = async (cmd: string) => {
|
||||
const preArgs = [process.argv[0], path.join(process.cwd(), 'bin', 'balena')];
|
||||
@ -147,3 +148,14 @@ export function fillTemplateArray(
|
||||
: fillTemplate(i, templateVars),
|
||||
);
|
||||
}
|
||||
|
||||
export async function switchSentry(
|
||||
enabled: boolean | undefined,
|
||||
): Promise<boolean | undefined> {
|
||||
const sentryOpts = (await setupSentry()).getClient()?.getOptions();
|
||||
if (sentryOpts) {
|
||||
const sentryStatus = sentryOpts.enabled;
|
||||
sentryOpts.enabled = enabled;
|
||||
return sentryStatus;
|
||||
}
|
||||
}
|
||||
|
@ -83,6 +83,29 @@ export class NockMock {
|
||||
return optional ? post.optionally() : post;
|
||||
}
|
||||
|
||||
protected inspectNoOp(_uri: string, _requestBody: nock.Body): void {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
protected getInspectedReplyBodyFunction(
|
||||
inspectRequest: (uri: string, requestBody: nock.Body) => void,
|
||||
replyBody: nock.ReplyBody,
|
||||
) {
|
||||
return function(
|
||||
this: nock.ReplyFnContext,
|
||||
uri: string,
|
||||
requestBody: nock.Body,
|
||||
cb: (err: NodeJS.ErrnoException | null, result: nock.ReplyBody) => void,
|
||||
) {
|
||||
try {
|
||||
inspectRequest(uri, requestBody);
|
||||
} catch (err) {
|
||||
cb(err, '');
|
||||
}
|
||||
cb(null, replyBody);
|
||||
};
|
||||
}
|
||||
|
||||
public done() {
|
||||
try {
|
||||
// scope.done() will throw an error if there are expected api calls that have not happened.
|
||||
|
@ -38,7 +38,7 @@ interface TarFiles {
|
||||
|
||||
const itSkipWindows = process.platform === 'win32' ? it.skip : it;
|
||||
|
||||
describe('compare new and old tarDirectory implementations', async function() {
|
||||
describe('compare new and old tarDirectory implementations', function() {
|
||||
const extraContent = 'extra';
|
||||
const extraEntry: tar.Headers = {
|
||||
name: 'extra.txt',
|
||||
|
Loading…
x
Reference in New Issue
Block a user