mirror of
https://github.com/balena-io/balena-cli.git
synced 2025-01-13 08:19:50 +00:00
Merge pull request #2274 from balena-io/deploy-release-versioning
Add balena.yml handling to `balena deploy` release creation
This commit is contained in:
commit
83f213c007
@ -259,3 +259,12 @@ gotchas to bear in mind:
|
|||||||
`node_modules/balena-sdk/node_modules/balena-errors`
|
`node_modules/balena-sdk/node_modules/balena-errors`
|
||||||
In the case of subclasses of `TypedError`, a string comparison may be used instead:
|
In the case of subclasses of `TypedError`, a string comparison may be used instead:
|
||||||
`error.name === 'BalenaApplicationNotFound'`
|
`error.name === 'BalenaApplicationNotFound'`
|
||||||
|
|
||||||
|
## Further debugging notes
|
||||||
|
|
||||||
|
* If you need to selectively run specific tests, `it.only` will not work in cases when authorization is required as part of the test cycle. In order to target specific tests, control execution via `.mocharc.js` instead. Here is an example of targeting the `deploy` tests.
|
||||||
|
|
||||||
|
replace: `spec: 'tests/**/*.spec.ts',`
|
||||||
|
|
||||||
|
with: `spec: ['tests/auth/*.spec.ts', 'tests/**/deploy.spec.ts'],`
|
||||||
|
|
||||||
|
@ -3369,6 +3369,13 @@ Set release tags if the image deployment is successful. Multiple
|
|||||||
arguments may be provided, alternating tag keys and values (see examples).
|
arguments may be provided, alternating tag keys and values (see examples).
|
||||||
Hint: Empty values may be specified with "" (bash, cmd.exe) or '""' (PowerShell).
|
Hint: Empty values may be specified with "" (bash, cmd.exe) or '""' (PowerShell).
|
||||||
|
|
||||||
|
#### --draft
|
||||||
|
|
||||||
|
Deploy the release as a draft. Draft releases are ignored
|
||||||
|
by the 'track latest' release policy but can be used through release pinning.
|
||||||
|
Draft releases can be marked as final through the API. Releases are created
|
||||||
|
as final by default unless this option is given.
|
||||||
|
|
||||||
#### -e, --emulated
|
#### -e, --emulated
|
||||||
|
|
||||||
Use QEMU for ARM architecture emulation during the image build
|
Use QEMU for ARM architecture emulation during the image build
|
||||||
|
@ -59,6 +59,7 @@ interface FlagsDef extends ComposeCliFlags, DockerCliFlags {
|
|||||||
build: boolean;
|
build: boolean;
|
||||||
nologupload: boolean;
|
nologupload: boolean;
|
||||||
'release-tag'?: string[];
|
'release-tag'?: string[];
|
||||||
|
draft: boolean;
|
||||||
help: void;
|
help: void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -136,6 +137,14 @@ ${dockerignoreHelp}
|
|||||||
`,
|
`,
|
||||||
multiple: true,
|
multiple: true,
|
||||||
}),
|
}),
|
||||||
|
draft: flags.boolean({
|
||||||
|
description: stripIndent`
|
||||||
|
Deploy the release as a draft. Draft releases are ignored
|
||||||
|
by the 'track latest' release policy but can be used through release pinning.
|
||||||
|
Draft releases can be marked as final through the API. Releases are created
|
||||||
|
as final by default unless this option is given.`,
|
||||||
|
default: false,
|
||||||
|
}),
|
||||||
...composeCliFlags,
|
...composeCliFlags,
|
||||||
...dockerCliFlags,
|
...dockerCliFlags,
|
||||||
// NOTE: Not supporting -h for help, because of clash with -h in DockerCliFlags
|
// NOTE: Not supporting -h for help, because of clash with -h in DockerCliFlags
|
||||||
@ -213,6 +222,7 @@ ${dockerignoreHelp}
|
|||||||
shouldPerformBuild: !!options.build,
|
shouldPerformBuild: !!options.build,
|
||||||
shouldUploadLogs: !options.nologupload,
|
shouldUploadLogs: !options.nologupload,
|
||||||
buildEmulated: !!options.emulated,
|
buildEmulated: !!options.emulated,
|
||||||
|
createAsDraft: options.draft,
|
||||||
buildOpts,
|
buildOpts,
|
||||||
});
|
});
|
||||||
await applyReleaseTagKeysAndValues(
|
await applyReleaseTagKeysAndValues(
|
||||||
@ -236,6 +246,7 @@ ${dockerignoreHelp}
|
|||||||
shouldUploadLogs: boolean;
|
shouldUploadLogs: boolean;
|
||||||
buildEmulated: boolean;
|
buildEmulated: boolean;
|
||||||
buildOpts: any; // arguments to forward to docker build command
|
buildOpts: any; // arguments to forward to docker build command
|
||||||
|
createAsDraft: boolean;
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
const _ = await import('lodash');
|
const _ = await import('lodash');
|
||||||
@ -367,6 +378,8 @@ ${dockerignoreHelp}
|
|||||||
`Bearer ${auth}`,
|
`Bearer ${auth}`,
|
||||||
apiEndpoint,
|
apiEndpoint,
|
||||||
!opts.shouldUploadLogs,
|
!opts.shouldUploadLogs,
|
||||||
|
composeOpts.projectPath,
|
||||||
|
opts.createAsDraft,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
3
lib/utils/compose-types.d.ts
vendored
3
lib/utils/compose-types.d.ts
vendored
@ -89,6 +89,9 @@ export interface Release {
|
|||||||
| 'commit'
|
| 'commit'
|
||||||
| 'composition'
|
| 'composition'
|
||||||
| 'source'
|
| 'source'
|
||||||
|
| 'is_final'
|
||||||
|
| 'contract'
|
||||||
|
| 'semver'
|
||||||
| 'start_timestamp'
|
| 'start_timestamp'
|
||||||
| 'end_timestamp'
|
| 'end_timestamp'
|
||||||
>;
|
>;
|
||||||
|
@ -175,6 +175,9 @@ export async function originalTarDirectory(dir, param) {
|
|||||||
* @param {number} userId
|
* @param {number} userId
|
||||||
* @param {number} appId
|
* @param {number} appId
|
||||||
* @param {import('resin-compose-parse').Composition} composition
|
* @param {import('resin-compose-parse').Composition} composition
|
||||||
|
* @param {boolean} draft
|
||||||
|
* @param {string|undefined} semver
|
||||||
|
* @param {string|undefined} contract
|
||||||
* @returns {Promise<import('./compose-types').Release>}
|
* @returns {Promise<import('./compose-types').Release>}
|
||||||
*/
|
*/
|
||||||
export const createRelease = async function (
|
export const createRelease = async function (
|
||||||
@ -183,6 +186,9 @@ export const createRelease = async function (
|
|||||||
userId,
|
userId,
|
||||||
appId,
|
appId,
|
||||||
composition,
|
composition,
|
||||||
|
draft,
|
||||||
|
semver,
|
||||||
|
contract,
|
||||||
) {
|
) {
|
||||||
const _ = require('lodash');
|
const _ = require('lodash');
|
||||||
const crypto = require('crypto');
|
const crypto = require('crypto');
|
||||||
@ -197,6 +203,9 @@ export const createRelease = async function (
|
|||||||
composition,
|
composition,
|
||||||
source: 'local',
|
source: 'local',
|
||||||
commit: crypto.pseudoRandomBytes(16).toString('hex').toLowerCase(),
|
commit: crypto.pseudoRandomBytes(16).toString('hex').toLowerCase(),
|
||||||
|
semver,
|
||||||
|
is_final: !draft,
|
||||||
|
contract,
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -207,6 +216,9 @@ export const createRelease = async function (
|
|||||||
'commit',
|
'commit',
|
||||||
'composition',
|
'composition',
|
||||||
'source',
|
'source',
|
||||||
|
'is_final',
|
||||||
|
'contract',
|
||||||
|
'semver',
|
||||||
'start_timestamp',
|
'start_timestamp',
|
||||||
'end_timestamp',
|
'end_timestamp',
|
||||||
]),
|
]),
|
||||||
|
@ -43,6 +43,9 @@ import type { DeviceInfo } from './device/api';
|
|||||||
import { getBalenaSdk, getChalk, stripIndent } from './lazy';
|
import { getBalenaSdk, getChalk, stripIndent } from './lazy';
|
||||||
import Logger = require('./logger');
|
import Logger = require('./logger');
|
||||||
import { exists } from './which';
|
import { exists } from './which';
|
||||||
|
import jsyaml = require('js-yaml');
|
||||||
|
|
||||||
|
const allowedContractTypes = ['sw.application', 'sw.block'];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Given an array representing the raw `--release-tag` flag of the deploy and
|
* Given an array representing the raw `--release-tag` flag of the deploy and
|
||||||
@ -1288,6 +1291,9 @@ async function pushServiceImages(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: This should be shared between the CLI & the Builder
|
||||||
|
const PLAIN_SEMVER_REGEX = /^([0-9]+)\.([0-9]+)\.([0-9]+)$/;
|
||||||
|
|
||||||
export async function deployProject(
|
export async function deployProject(
|
||||||
docker: import('dockerode'),
|
docker: import('dockerode'),
|
||||||
logger: Logger,
|
logger: Logger,
|
||||||
@ -1298,6 +1304,8 @@ export async function deployProject(
|
|||||||
auth: string,
|
auth: string,
|
||||||
apiEndpoint: string,
|
apiEndpoint: string,
|
||||||
skipLogUpload: boolean,
|
skipLogUpload: boolean,
|
||||||
|
projectPath: string,
|
||||||
|
isDraft: boolean,
|
||||||
): Promise<import('balena-release/build/models').ReleaseModel> {
|
): Promise<import('balena-release/build/models').ReleaseModel> {
|
||||||
const releaseMod = await import('balena-release');
|
const releaseMod = await import('balena-release');
|
||||||
const { createRelease, tagServiceImages } = await import('./compose');
|
const { createRelease, tagServiceImages } = await import('./compose');
|
||||||
@ -1306,11 +1314,29 @@ export async function deployProject(
|
|||||||
const prefix = getChalk().cyan('[Info]') + ' ';
|
const prefix = getChalk().cyan('[Info]') + ' ';
|
||||||
const spinner = createSpinner();
|
const spinner = createSpinner();
|
||||||
|
|
||||||
|
const contractPath = path.join(projectPath, 'balena.yml');
|
||||||
|
const contract = await getContractContent(contractPath);
|
||||||
|
if (contract?.version && !PLAIN_SEMVER_REGEX.test(contract?.version)) {
|
||||||
|
throw new ExpectedError(stripIndent`\
|
||||||
|
Error: expected the version field in "${contractPath}"
|
||||||
|
to be a basic semver in the format '1.2.3'. Got '${contract.version}' instead`);
|
||||||
|
}
|
||||||
|
|
||||||
const $release = await runSpinner(
|
const $release = await runSpinner(
|
||||||
tty,
|
tty,
|
||||||
spinner,
|
spinner,
|
||||||
`${prefix}Creating release...`,
|
`${prefix}Creating release...`,
|
||||||
() => createRelease(apiEndpoint, auth, userId, appId, composition),
|
() =>
|
||||||
|
createRelease(
|
||||||
|
apiEndpoint,
|
||||||
|
auth,
|
||||||
|
userId,
|
||||||
|
appId,
|
||||||
|
composition,
|
||||||
|
isDraft,
|
||||||
|
contract?.version,
|
||||||
|
contract ? JSON.stringify(contract) : undefined,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
const { client: pineClient, release, serviceImages } = $release;
|
const { client: pineClient, release, serviceImages } = $release;
|
||||||
|
|
||||||
@ -1395,6 +1421,42 @@ export function createRunLoop(tick: (...args: any[]) => void) {
|
|||||||
return runloop;
|
return runloop;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function getContractContent(
|
||||||
|
filePath: string,
|
||||||
|
): Promise<Dictionary<any> | undefined> {
|
||||||
|
let fileContentAsString;
|
||||||
|
try {
|
||||||
|
fileContentAsString = await fs.readFile(filePath, 'utf8');
|
||||||
|
} catch (e) {
|
||||||
|
if (e.code === 'ENOENT') {
|
||||||
|
return; // File does not exist
|
||||||
|
}
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
|
||||||
|
let asJson;
|
||||||
|
try {
|
||||||
|
asJson = jsyaml.load(fileContentAsString);
|
||||||
|
} catch (err) {
|
||||||
|
throw new ExpectedError(
|
||||||
|
`Error parsing file "${filePath}":\n ${err.message}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isContract(asJson)) {
|
||||||
|
throw new ExpectedError(
|
||||||
|
stripIndent`Error: application contract in '${filePath}' needs to
|
||||||
|
define a top level "type" field with an allowed application type.
|
||||||
|
Allowed application types are: ${allowedContractTypes.join(', ')}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return asJson;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isContract(obj: any): obj is Dictionary<any> {
|
||||||
|
return obj?.type && allowedContractTypes.includes(obj.type);
|
||||||
|
}
|
||||||
|
|
||||||
function createLogStream(input: Readable) {
|
function createLogStream(input: Readable) {
|
||||||
const split = require('split') as typeof import('split');
|
const split = require('split') as typeof import('split');
|
||||||
const stripAnsi = require('strip-ansi-stream');
|
const stripAnsi = require('strip-ansi-stream');
|
||||||
|
6
npm-shrinkwrap.json
generated
6
npm-shrinkwrap.json
generated
@ -3969,9 +3969,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"balena-release": {
|
"balena-release": {
|
||||||
"version": "3.0.1",
|
"version": "3.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/balena-release/-/balena-release-3.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/balena-release/-/balena-release-3.2.0.tgz",
|
||||||
"integrity": "sha512-xzHwTe9zp7Dw4JLPoEGzTseAG1oQNDCAfbEXT7QrzyfXedlqKx/XyP+HoR7Q2ykIVMNUOBm3+7y2/ThMp2sEMw==",
|
"integrity": "sha512-jwmAjIZCJ5I46/yQNN+dA73RWlre0+jBVmo2QeJl1pK83obTLyifJeWNVf5irzP8KFE7WQzo9ICK1cCpLtygFA==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"@types/bluebird": "^3.5.18",
|
"@types/bluebird": "^3.5.18",
|
||||||
"@types/node": "^8.0.55",
|
"@types/node": "^8.0.55",
|
||||||
|
@ -206,7 +206,7 @@
|
|||||||
"balena-image-fs": "^7.0.6",
|
"balena-image-fs": "^7.0.6",
|
||||||
"balena-image-manager": "^7.0.3",
|
"balena-image-manager": "^7.0.3",
|
||||||
"balena-preload": "^10.5.0",
|
"balena-preload": "^10.5.0",
|
||||||
"balena-release": "^3.0.1",
|
"balena-release": "^3.2.0",
|
||||||
"balena-sdk": "^15.48.0",
|
"balena-sdk": "^15.48.0",
|
||||||
"balena-semver": "^2.3.0",
|
"balena-semver": "^2.3.0",
|
||||||
"balena-settings-client": "^4.0.7",
|
"balena-settings-client": "^4.0.7",
|
||||||
|
@ -15,9 +15,11 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import type { Request as ReleaseRequest } from 'balena-release';
|
||||||
import { expect } from 'chai';
|
import { expect } from 'chai';
|
||||||
import { promises as fs } from 'fs';
|
import { promises as fs } from 'fs';
|
||||||
import * as _ from 'lodash';
|
import * as _ from 'lodash';
|
||||||
|
import * as nock from 'nock';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import * as sinon from 'sinon';
|
import * as sinon from 'sinon';
|
||||||
|
|
||||||
@ -79,7 +81,6 @@ describe('balena deploy', function () {
|
|||||||
api.expectGetMixpanel({ optional: true });
|
api.expectGetMixpanel({ optional: true });
|
||||||
api.expectGetConfigDeviceTypes();
|
api.expectGetConfigDeviceTypes();
|
||||||
api.expectGetApplication();
|
api.expectGetApplication();
|
||||||
api.expectPostRelease();
|
|
||||||
api.expectGetRelease();
|
api.expectGetRelease();
|
||||||
api.expectGetUser();
|
api.expectGetUser();
|
||||||
api.expectGetService({ serviceName: 'main' });
|
api.expectGetService({ serviceName: 'main' });
|
||||||
@ -137,6 +138,7 @@ describe('balena deploy', function () {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
api.expectPostRelease({});
|
||||||
api.expectPatchImage({});
|
api.expectPatchImage({});
|
||||||
api.expectPatchRelease({});
|
api.expectPatchRelease({});
|
||||||
api.expectPostImageLabel();
|
api.expectPostImageLabel();
|
||||||
@ -154,6 +156,103 @@ describe('balena deploy', function () {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should handle the contract and final status for a final (non-draft) release', async () => {
|
||||||
|
const projectPath = path.join(
|
||||||
|
projectsPath,
|
||||||
|
'no-docker-compose',
|
||||||
|
'with-contract',
|
||||||
|
);
|
||||||
|
const expectedFiles: ExpectedTarStreamFiles = {
|
||||||
|
'src/start.sh': { fileSize: 30, type: 'file' },
|
||||||
|
Dockerfile: { fileSize: 88, type: 'file' },
|
||||||
|
'balena.yml': { fileSize: 55, type: 'file' },
|
||||||
|
};
|
||||||
|
const responseFilename = 'build-POST.json';
|
||||||
|
const responseBody = await fs.readFile(
|
||||||
|
path.join(dockerResponsePath, responseFilename),
|
||||||
|
'utf8',
|
||||||
|
);
|
||||||
|
const expectedResponseLines = [
|
||||||
|
...commonResponseLines[responseFilename],
|
||||||
|
`[Info] No "docker-compose.yml" file found at "${projectPath}"`,
|
||||||
|
`[Info] Creating default composition with source: "${projectPath}"`,
|
||||||
|
];
|
||||||
|
|
||||||
|
api.expectPostRelease({
|
||||||
|
inspectRequest: (_uri: string, requestBody: nock.Body) => {
|
||||||
|
const body = requestBody.valueOf() as Partial<ReleaseRequest>;
|
||||||
|
expect(body.contract).to.be.equal(
|
||||||
|
'{"name":"testContract","type":"sw.application","version":"1.5.2"}',
|
||||||
|
);
|
||||||
|
expect(body.is_final).to.be.true;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
api.expectPatchImage({});
|
||||||
|
api.expectPatchRelease({});
|
||||||
|
api.expectPostImageLabel();
|
||||||
|
|
||||||
|
await testDockerBuildStream({
|
||||||
|
commandLine: `deploy testApp --build --source ${projectPath}`,
|
||||||
|
dockerMock: docker,
|
||||||
|
expectedFilesByService: { main: expectedFiles },
|
||||||
|
expectedQueryParamsByService: { main: commonQueryParams },
|
||||||
|
expectedResponseLines,
|
||||||
|
projectPath,
|
||||||
|
responseBody,
|
||||||
|
responseCode: 200,
|
||||||
|
services: ['main'],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle the contract and final status for a draft release', async () => {
|
||||||
|
const projectPath = path.join(
|
||||||
|
projectsPath,
|
||||||
|
'no-docker-compose',
|
||||||
|
'with-contract',
|
||||||
|
);
|
||||||
|
const expectedFiles: ExpectedTarStreamFiles = {
|
||||||
|
'src/start.sh': { fileSize: 30, type: 'file' },
|
||||||
|
Dockerfile: { fileSize: 88, type: 'file' },
|
||||||
|
'balena.yml': { fileSize: 55, type: 'file' },
|
||||||
|
};
|
||||||
|
const responseFilename = 'build-POST.json';
|
||||||
|
const responseBody = await fs.readFile(
|
||||||
|
path.join(dockerResponsePath, responseFilename),
|
||||||
|
'utf8',
|
||||||
|
);
|
||||||
|
const expectedResponseLines = [
|
||||||
|
...commonResponseLines[responseFilename],
|
||||||
|
`[Info] No "docker-compose.yml" file found at "${projectPath}"`,
|
||||||
|
`[Info] Creating default composition with source: "${projectPath}"`,
|
||||||
|
];
|
||||||
|
|
||||||
|
api.expectPostRelease({
|
||||||
|
inspectRequest: (_uri: string, requestBody: nock.Body) => {
|
||||||
|
const body = requestBody.valueOf() as Partial<ReleaseRequest>;
|
||||||
|
expect(body.contract).to.be.equal(
|
||||||
|
'{"name":"testContract","type":"sw.application","version":"1.5.2"}',
|
||||||
|
);
|
||||||
|
expect(body.semver).to.be.equal('1.5.2');
|
||||||
|
expect(body.is_final).to.be.false;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
api.expectPatchImage({});
|
||||||
|
api.expectPatchRelease({});
|
||||||
|
api.expectPostImageLabel();
|
||||||
|
|
||||||
|
await testDockerBuildStream({
|
||||||
|
commandLine: `deploy testApp --build --draft --source ${projectPath}`,
|
||||||
|
dockerMock: docker,
|
||||||
|
expectedFilesByService: { main: expectedFiles },
|
||||||
|
expectedQueryParamsByService: { main: commonQueryParams },
|
||||||
|
expectedResponseLines,
|
||||||
|
projectPath,
|
||||||
|
responseBody,
|
||||||
|
responseCode: 200,
|
||||||
|
services: ['main'],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it('should update a release with status="failed" on error (single container)', async () => {
|
it('should update a release with status="failed" on error (single container)', async () => {
|
||||||
let sentryStatus: boolean | undefined;
|
let sentryStatus: boolean | undefined;
|
||||||
const projectPath = path.join(projectsPath, 'no-docker-compose', 'basic');
|
const projectPath = path.join(projectsPath, 'no-docker-compose', 'basic');
|
||||||
@ -176,6 +275,8 @@ describe('balena deploy', function () {
|
|||||||
// causes the CLI to call process.exit() with process.exitCode = 1
|
// causes the CLI to call process.exit() with process.exitCode = 1
|
||||||
const expectedExitCode = 1;
|
const expectedExitCode = 1;
|
||||||
|
|
||||||
|
api.expectPostRelease({});
|
||||||
|
|
||||||
// Mock this patch HTTP request to return status code 500, in which case
|
// Mock this patch HTTP request to return status code 500, in which case
|
||||||
// the release status should be saved as "failed" rather than "success"
|
// the release status should be saved as "failed" rather than "success"
|
||||||
api.expectPatchImage({
|
api.expectPatchImage({
|
||||||
@ -302,7 +403,7 @@ describe('balena deploy', function () {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// docker.expectGetImages();
|
api.expectPostRelease({});
|
||||||
api.expectPatchImage({});
|
api.expectPatchImage({});
|
||||||
api.expectPatchRelease({});
|
api.expectPatchRelease({});
|
||||||
|
|
||||||
|
@ -43,3 +43,7 @@ import { config as chaiCfg } from 'chai';
|
|||||||
chaiCfg.showDiff = true;
|
chaiCfg.showDiff = true;
|
||||||
// enable diff comparison of large objects / arrays
|
// enable diff comparison of large objects / arrays
|
||||||
chaiCfg.truncateThreshold = 0;
|
chaiCfg.truncateThreshold = 0;
|
||||||
|
// Because mocks are pointed at "production", we need to make sure this is set to prod.
|
||||||
|
// Otherwise if the user has BALENARC_BALENA_URL pointing at something else like staging, tests
|
||||||
|
// will fail.
|
||||||
|
process.env.BALENARC_BALENA_URL = 'balena-cloud.com';
|
||||||
|
@ -122,11 +122,18 @@ export class BalenaAPIMock extends NockMock {
|
|||||||
/**
|
/**
|
||||||
* Mocks balena-release call
|
* Mocks balena-release call
|
||||||
*/
|
*/
|
||||||
public expectPostRelease(opts: ScopeOpts = {}) {
|
public expectPostRelease({
|
||||||
this.optPost(/^\/v6\/release($|[(?])/, opts).replyWithFile(
|
statusCode = 200,
|
||||||
200,
|
inspectRequest = this.inspectNoOp,
|
||||||
|
optional = false,
|
||||||
|
persist = false,
|
||||||
|
}) {
|
||||||
|
this.optPost(/^\/v6\/release($|[(?])/, { optional, persist }).reply(
|
||||||
|
statusCode,
|
||||||
|
this.getInspectedReplyFileFunction(
|
||||||
|
inspectRequest,
|
||||||
path.join(apiResponsePath, 'release-POST-v6.json'),
|
path.join(apiResponsePath, 'release-POST-v6.json'),
|
||||||
jHeader,
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -16,6 +16,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import * as nock from 'nock';
|
import * as nock from 'nock';
|
||||||
|
import * as fs from 'fs';
|
||||||
|
|
||||||
export interface ScopeOpts {
|
export interface ScopeOpts {
|
||||||
optional?: boolean;
|
optional?: boolean;
|
||||||
@ -103,6 +104,27 @@ export class NockMock {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected getInspectedReplyFileFunction(
|
||||||
|
inspectRequest: (uri: string, requestBody: nock.Body) => void,
|
||||||
|
replyBodyFile: string,
|
||||||
|
) {
|
||||||
|
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, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
const replyBody = fs.readFileSync(replyBodyFile);
|
||||||
|
cb(null, replyBody);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
public done() {
|
public done() {
|
||||||
try {
|
try {
|
||||||
// scope.done() will throw an error if there are expected api calls that have not happened.
|
// scope.done() will throw an error if there are expected api calls that have not happened.
|
||||||
|
@ -0,0 +1,4 @@
|
|||||||
|
FROM busybox
|
||||||
|
COPY ./src /usr/src/
|
||||||
|
RUN chmod a+x /usr/src/*.sh
|
||||||
|
CMD ["/usr/src/start.sh"]
|
@ -0,0 +1,3 @@
|
|||||||
|
name: testContract
|
||||||
|
type: sw.application
|
||||||
|
version: 1.5.2
|
2
tests/test-data/projects/no-docker-compose/with-contract/src/start.sh
Executable file
2
tests/test-data/projects/no-docker-compose/with-contract/src/start.sh
Executable file
@ -0,0 +1,2 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
echo "Hello, test!"
|
Loading…
Reference in New Issue
Block a user