Compare commits

...

26 Commits

Author SHA1 Message Date
3f9288e9d3 v22.1.1 2025-06-19 09:32:56 +00:00
0efa745628 Merge pull request #2957 from balena-io/truncate-error-message
Deploy: Limit the submitted error_message of images that fail to build to 300K characters
2025-06-19 09:31:58 +00:00
bddad252f7 Deploy: Limit the submitted error_message of images that fail to build to 1000 characters
Change-type: patch
See: https://balena.fibery.io/Work/Project/re-pitching-API-Limit-size-of-large-fields-975
2025-06-19 11:28:38 +03:00
a1a0e4f028 Deduplicate dependencies 2025-06-17 23:47:57 +03:00
de74baa2ff v22.1.0 2025-06-09 20:05:12 +00:00
6c12f755c5 Merge pull request #2955 from balena-io/node-22
Node 22
2025-06-09 17:04:26 -03:00
f80b8e63b1 Add support for node 22
Change-type: minor
2025-06-07 11:14:48 -03:00
b32514f5af Bump etcher-sdk to v10.0.0
Update balena-device-init from 8.1.3 to 8.1.1
Update etcher-sdk from 9.1.4 to 10.0.0
Update resin-cli-form from 3.0.0 to 4.0.0
Update resin-cli-visuals from 2.0.1 to 3.0.0

Change-type: patch
2025-06-07 11:13:55 -03:00
935f8d2549 v22.0.6 2025-06-02 12:27:13 +00:00
b2de857ef1 Merge pull request #2952 from balena-io/remove-request
Remove request
2025-06-02 08:26:26 -04:00
78f1471bf4 Deduplicate dependencies 2025-05-30 08:23:23 -04:00
d47abf072d Remove request dependency
Change-type: patch
2025-05-30 08:16:08 -04:00
8502c4db4b Replace request usage with got
Change-type: patch
2025-05-30 08:16:08 -04:00
dd2c5c40d7 v22.0.5 2025-05-29 15:47:35 +00:00
d23b253ac5 Merge pull request #2951 from balena-io/bump-etcher-sdk
Bump etcher-sdk to v9.1.4
2025-05-29 12:46:43 -03:00
0b0e24c9b2 Bump etcher-sdk to v9.1.4
Update etcher-sdk from 9.1.0 to 9.1.4

Change-type: patch
2025-05-29 10:54:45 -03:00
f9656cbe91 v22.0.4 2025-05-29 13:19:04 +00:00
e174f7db4c Merge pull request #2949 from balena-io/use-got-instead-of-request-legacy-deploy
Use got instead of request legacy deploy
2025-05-29 13:18:10 +00:00
a7a408a5c7 tests: Replace request with got
Change-type: patch
2025-05-29 09:35:57 -03:00
e5877c7de9 deploy-legacy: Replace request with got
Change-type: patch
2025-05-29 09:35:57 -03:00
ecb8b3ae6b v22.0.3 2025-05-29 12:09:59 +00:00
fd20516f69 Merge pull request #2950 from balena-io/bump-sentry-v9
Bump sentry to v9
2025-05-29 12:09:11 +00:00
5ccee0e4f1 Bump sentry to v9
Change-type: patch
2025-05-28 20:29:40 -03:00
7bb13a551c v22.0.2 2025-05-28 19:32:18 +00:00
19d287aefc Merge pull request #2948 from balena-io/build-nologs
fix: allow balena build to work with --nologs
2025-05-28 16:31:28 -03:00
8d10c1af2a Fix balena build to work with --nologs
Change-type: patch
2025-05-28 15:36:56 -03:00
20 changed files with 1587 additions and 1171 deletions

View File

@ -18,7 +18,7 @@ inputs:
default: 'accounts+apple@balena.io'
NODE_VERSION:
type: string
default: '20.x'
default: '22.x'
VERBOSE:
type: string
default: 'true'

View File

@ -15,7 +15,7 @@ inputs:
# --- custom environment
NODE_VERSION:
type: string
default: '20.x'
default: '22.x'
VERBOSE:
type: string
default: "true"

View File

@ -1,3 +1,192 @@
- commits:
- subject: "Deploy: Limit the submitted error_message of images that fail to build
to 1000 characters"
hash: bddad252f7cb412a3d417be1d7bd7e4ed9726b8e
body: ""
footer:
Change-type: patch
change-type: patch
See: https://balena.fibery.io/Work/Project/re-pitching-API-Limit-size-of-large-fields-975
see: https://balena.fibery.io/Work/Project/re-pitching-API-Limit-size-of-large-fields-975
author: Thodoris Greasidis
nested: []
version: 22.1.1
title: ""
date: 2025-06-19T09:32:52.976Z
- commits:
- subject: Add support for node 22
hash: f80b8e63b1e5b22dd95b034fe14da0a7e3ab4986
body: ""
footer:
Change-type: minor
change-type: minor
author: Otavio Jacobi
nested: []
- subject: Bump etcher-sdk to v10.0.0
hash: b32514f5afb4fdd12e4a9dca5e2a1d53e727b434
body: |
Update balena-device-init from 8.1.3 to 8.1.1
Update etcher-sdk from 9.1.4 to 10.0.0
Update resin-cli-form from 3.0.0 to 4.0.0
Update resin-cli-visuals from 2.0.1 to 3.0.0
footer:
Change-type: patch
change-type: patch
author: Otavio Jacobi
nested:
- commits:
- subject: Drop support to node18 and add support to node 22 & 24
hash: 95e577823f642a6c0e500aa29fc150b7807d84f7
body: ""
footer:
Change-type: major
change-type: major
author: Otavio Jacobi
nested: []
version: etcher-sdk-10.0.0
title: ""
date: 2025-06-02T09:12:32.868Z
version: 22.1.0
title: ""
date: 2025-06-09T20:05:08.020Z
- commits:
- subject: Remove `request` dependency
hash: d47abf072ddf1f6529f3d4a14e07436def58df61
body: ""
footer:
Change-type: patch
change-type: patch
author: myarmolinsky
nested: []
- subject: Replace `request` usage with `got`
hash: 8502c4db4bb211a70c682dbc1b85df56f01f2d93
body: ""
footer:
Change-type: patch
change-type: patch
author: myarmolinsky
nested: []
version: 22.0.6
title: ""
date: 2025-06-02T12:27:11.068Z
- commits:
- subject: Bump etcher-sdk to v9.1.4
hash: 0b0e24c9b29ef4bcb6a577ca85708171cc2421c7
body: |
Update etcher-sdk from 9.1.0 to 9.1.4
footer:
Change-type: patch
change-type: patch
author: Otavio Jacobi
nested:
- commits:
- subject: Run `npm audit fix` which should only do non-breaking changes
hash: 22aaacc0744e41989706c968c4efc8767d30b7a8
body: ""
footer:
Change-type: patch
change-type: patch
author: Otavio Jacobi
nested: []
version: etcher-sdk-9.1.4
title: ""
date: 2025-05-29T08:57:28.785Z
- commits:
- subject: Embed config.json with a fixed timestamp to enable consistent checksums
hash: 83e67a4089ec39023c39fe79fe59021237797c85
body: >
Previously the timestamp was changing each time which meant the
checksum would be different every time even if the rest of the
contents
were identical. Changing this to a fixed timestamp avoids that
change
such that only the contents matter.
footer:
Change-type: patch
change-type: patch
author: Pagan Gazzard
nested: []
version: etcher-sdk-9.1.3
title: ""
date: 2025-02-17T12:48:33.911Z
- commits:
- subject: Update dependency unzip-stream to v0.3.2 [SECURITY]
hash: c243e56e4189bee7391943a3325a3c1465c62fd1
body: |
Update unzip-stream from 0.3.1 to 0.3.2
footer:
Change-type: patch
change-type: patch
author: Self-hosted Renovate Bot
nested: []
version: etcher-sdk-9.1.2
title: ""
date: 2024-10-09T08:52:13.524Z
- commits:
- subject: "patch: add EXLOCK flag for windows"
hash: 915feeeceff83249f87a6a0a1656986791206136
body: |
Signed-off-by: Talha Can Havadar <havadartalha@gmail.com>
run prettier for changed files
footer:
Signed-off-by: Talha Can Havadar <havadartalha@gmail.com>
signed-off-by: Talha Can Havadar <havadartalha@gmail.com>
author: Talha Can Havadar
nested: []
version: etcher-sdk-9.1.1
title: ""
date: 2024-10-09T08:24:06.706Z
version: 22.0.5
title: ""
date: 2025-05-29T15:47:31.891Z
- commits:
- subject: "tests: Replace request with got"
hash: a7a408a5c7dcf06b770e8df85e250bfed5a09f75
body: ""
footer:
Change-type: patch
change-type: patch
author: Otavio Jacobi
nested: []
- subject: "deploy-legacy: Replace request with got"
hash: e5877c7de917e377082328ee8ab0b502593c9719
body: ""
footer:
Change-type: patch
change-type: patch
author: Otavio Jacobi
nested: []
version: 22.0.4
title: ""
date: 2025-05-29T13:19:01.228Z
- commits:
- subject: Bump sentry to v9
hash: 5ccee0e4f1ce3bae6963630963cbd72c9c738f77
body: ""
footer:
Change-type: patch
change-type: patch
author: Otavio Jacobi
nested: []
version: 22.0.3
title: ""
date: 2025-05-29T12:09:56.743Z
- commits:
- subject: Fix balena build to work with --nologs
hash: 8d10c1af2a8eddfa146e2d23161c079127eb5546
body: ""
footer:
Change-type: patch
change-type: patch
author: Otavio Jacobi
nested: []
version: 22.0.2
title: ""
date: 2025-05-28T19:32:15.230Z
- commits:
- subject: "DeviceAPI: Move away from `request` in favor of BalenaSdk request"
hash: 3396ba5a971d2ae16552576d65dff953031f01ee

View File

@ -4,6 +4,67 @@ All notable changes to this project will be documented in this file
automatically by Versionist. DO NOT EDIT THIS FILE MANUALLY!
This project adheres to [Semantic Versioning](http://semver.org/).
## 22.1.1 - 2025-06-19
* Deploy: Limit the submitted error_message of images that fail to build to 1000 characters [Thodoris Greasidis]
## 22.1.0 - 2025-06-09
* Add support for node 22 [Otavio Jacobi]
<details>
<summary> Bump etcher-sdk to v10.0.0 [Otavio Jacobi] </summary>
> ### etcher-sdk-10.0.0 - 2025-06-02
>
> * Drop support to node18 and add support to node 22 & 24 [Otavio Jacobi]
>
</details>
## 22.0.6 - 2025-06-02
* Remove `request` dependency [myarmolinsky]
* Replace `request` usage with `got` [myarmolinsky]
## 22.0.5 - 2025-05-29
<details>
<summary> Bump etcher-sdk to v9.1.4 [Otavio Jacobi] </summary>
> ### etcher-sdk-9.1.4 - 2025-05-29
>
> * Run `npm audit fix` which should only do non-breaking changes [Otavio Jacobi]
>
> ### etcher-sdk-9.1.3 - 2025-02-17
>
> * Embed config.json with a fixed timestamp to enable consistent checksums [Pagan Gazzard]
>
> ### etcher-sdk-9.1.2 - 2024-10-09
>
> * Update dependency unzip-stream to v0.3.2 [SECURITY] [Self-hosted Renovate Bot]
>
> ### etcher-sdk-9.1.1 - 2024-10-09
>
> * patch: add EXLOCK flag for windows [Talha Can Havadar]
>
</details>
## 22.0.4 - 2025-05-29
* tests: Replace request with got [Otavio Jacobi]
* deploy-legacy: Replace request with got [Otavio Jacobi]
## 22.0.3 - 2025-05-29
* Bump sentry to v9 [Otavio Jacobi]
## 22.0.2 - 2025-05-28
* Fix balena build to work with --nologs [Otavio Jacobi]
## 22.0.1 - 2025-05-28
* DeviceAPI: Move away from `request` in favor of BalenaSdk request [myarmolinsky]

View File

@ -77,8 +77,8 @@ If you are a Node.js developer, you may wish to install the balena CLI via [npm]
The npm installation involves building native (platform-specific) binary modules, which require
some development tools to be installed first, as follows.
> **The balena CLI currently requires Node.js version ^20.6.0**
> **Versions 21 and later are not yet fully supported.**
> **The balena CLI currently requires Node.js version >=20.6.0**
> **Versions 23 and later are not yet fully supported.**
### Install development tools
@ -88,7 +88,7 @@ some development tools to be installed first, as follows.
$ sudo apt-get update && sudo apt-get -y install curl python3 git make g++
$ curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.38.0/install.sh | bash
$ . ~/.bashrc
$ nvm install 20
$ nvm install 22
```
The `curl` command line above uses
@ -105,7 +105,7 @@ recommended.
```sh
$ curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.38.0/install.sh | bash
$ . ~/.bashrc
$ nvm install 20
$ nvm install 22
```
#### **Windows** (not WSL)
@ -113,7 +113,7 @@ $ nvm install 20
Install:
* If you'd like the ability to switch between Node.js versions, install
- Node.js v20 from the [Nodejs.org releases page](https://nodejs.org/en/download/releases/).
- Node.js v22 from the [Nodejs.org releases page](https://nodejs.org/en/download/releases/).
[nvm-windows](https://github.com/coreybutler/nvm-windows#node-version-manager-nvm-for-windows)
instead.
* The [MSYS2 shell](https://www.msys2.org/), which provides `git`, `make`, `g++` and more:

2089
npm-shrinkwrap.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
{
"name": "balena-cli",
"version": "22.0.1",
"version": "22.1.1",
"description": "The official balena Command Line Interface",
"main": "./build/app.js",
"homepage": "https://github.com/balena-io/balena-cli",
@ -72,7 +72,7 @@
"author": "Balena Inc. (https://balena.io/)",
"license": "Apache-2.0",
"engines": {
"node": "^20.6.0"
"node": ">=20.6.0 <23"
},
"oclif": {
"bin": "balena",
@ -124,7 +124,6 @@
"@types/node-cleanup": "^2.1.2",
"@types/prettyjson": "^0.0.33",
"@types/progress-stream": "^2.0.2",
"@types/request": "^2.48.7",
"@types/rewire": "^2.5.30",
"@types/rimraf": "^3.0.2",
"@types/semver": "^7.3.9",
@ -169,9 +168,9 @@
"@balena/env-parsing": "^1.1.8",
"@balena/es-version": "^1.0.1",
"@oclif/core": "^4.1.0",
"@sentry/node": "^6.16.1",
"@sentry/node": "^9.0.0",
"balena-config-json": "^4.2.7",
"balena-device-init": "^8.1.3",
"balena-device-init": "^8.1.11",
"balena-errors": "^4.7.3",
"balena-image-fs": "^7.5.2",
"balena-preload": "^18.0.4",
@ -192,7 +191,7 @@
"docker-progress": "^5.1.3",
"dockerode": "^4.0.5",
"ejs": "^3.1.6",
"etcher-sdk": "9.1.0",
"etcher-sdk": "^10.0.0",
"express": "^4.17.2",
"fast-boot2": "^1.1.0",
"fast-levenshtein": "^3.0.0",
@ -220,9 +219,8 @@
"prettyjson": "^1.2.5",
"progress-stream": "^2.0.0",
"reconfix": "^1.0.0-v0-1-0-fork-46760acff4d165f5238bfac5e464256ef1944476",
"request": "^2.88.2",
"resin-cli-form": "^3.0.0",
"resin-cli-visuals": "^2.0.1",
"resin-cli-form": "^4.0.0",
"resin-cli-visuals": "^3.0.0",
"resin-doodles": "^0.2.0",
"resin-stream-logger": "^0.1.2",
"rimraf": "^3.0.2",
@ -250,6 +248,6 @@
}
},
"versionist": {
"publishedAt": "2025-05-28T17:00:46.792Z"
"publishedAt": "2025-06-19T09:32:53.877Z"
}
}

View File

@ -34,18 +34,14 @@ export const setupSentry = onceAsync(async () => {
const config = await import('./config');
const Sentry = await import('@sentry/node');
Sentry.init({
autoSessionTracking: false,
dsn: config.sentryDsn,
release: packageJSON.version,
});
Sentry.configureScope((scope) => {
scope.setExtras({
is_pkg: !!(process as any).pkg,
node_version: process.version,
platform: process.platform,
});
Sentry.getCurrentScope().setExtras({
is_pkg: !!(process as any).pkg,
node_version: process.version,
platform: process.platform,
});
return Sentry.getCurrentHub();
});
async function checkNodeVersion() {

View File

@ -38,11 +38,11 @@ import { stripIndent } from './utils/lazy';
export async function trackCommand(commandSignature: string) {
try {
let Sentry: typeof import('@sentry/node');
let scope: import('@sentry/node').Scope;
if (!process.env.BALENARC_NO_SENTRY) {
Sentry = await import('@sentry/node');
Sentry.configureScope((scope) => {
scope.setExtra('command', commandSignature);
});
scope = Sentry.getCurrentScope();
scope.setExtra('command', commandSignature);
}
const { getCachedUsername } = await import('./utils/bootstrap');
let username: string | undefined;
@ -52,11 +52,9 @@ export async function trackCommand(commandSignature: string) {
// ignore
}
if (!process.env.BALENARC_NO_SENTRY) {
Sentry!.configureScope((scope) => {
scope.setUser({
id: username,
username,
});
scope!.setUser({
id: username,
username,
});
}
if (

View File

@ -1322,6 +1322,9 @@ async function pushAndUpdateServiceImages(
}
}
// Error messages are limited to 300KB characters in the API, so we truncate longer ones.
const MAX_ERROR_MESSAGE_LENGTH = 300_000;
async function pushServiceImages(
docker: Dockerode,
logger: Logger,
@ -1344,23 +1347,34 @@ async function pushServiceImages(
delete serviceImage.build_log;
}
await releaseMod.updateImage(
pineClient,
serviceImage.id,
// These are the only update-able image fields in bC atm, and passing
// the whole image object in v7+ would result the allowlist to reject the request.
_.pick(serviceImage, [
'end_timestamp',
'project_type',
'error_message',
'build_log',
'push_timestamp',
'status',
'content_hash',
'dockerfile',
'image_size',
]),
);
// These are the only update-able image fields in bC atm, and passing
// the whole image object in v7+ would result the allowlist to reject the request.
const imagePayload = _.pick(serviceImage, [
'end_timestamp',
'project_type',
'error_message',
'build_log',
'push_timestamp',
'status',
'content_hash',
'dockerfile',
'image_size',
]);
if (
typeof imagePayload.error_message === 'string' &&
imagePayload.error_message.length > MAX_ERROR_MESSAGE_LENGTH
) {
logger.logDebug(
`Truncating error message of image ${serviceImage.is_stored_at__image_location} to ${MAX_ERROR_MESSAGE_LENGTH} characters.`,
);
imagePayload.error_message = imagePayload.error_message.substring(
0,
MAX_ERROR_MESSAGE_LENGTH,
);
}
await releaseMod.updateImage(pineClient, serviceImage.id, imagePayload);
},
);
}
@ -1596,7 +1610,9 @@ function buildProgressAdapter(inline: boolean) {
return;
}
if (!str.startsWith('Successfully tagged ')) {
// We want to keep the regex match instead of startsWith as it also works with buffers
// eslint-disable-next-line @typescript-eslint/prefer-string-starts-ends-with
if (!/^Successfully tagged /.test(str)) {
const match = stepRegex.exec(str);
if (match) {
step = match[1];

View File

@ -19,7 +19,7 @@ import { getVisuals } from './lazy';
import { promisify } from 'util';
import type * as Dockerode from 'dockerode';
import type Logger = require('./logger');
import type { Request } from 'request';
import type got from 'got';
const getBuilderPushEndpoint = function (
baseUrl: string,
@ -75,7 +75,10 @@ const showPushProgress = function (message: string) {
return progressBar;
};
const uploadToPromise = (uploadRequest: Request, logger: Logger) =>
const uploadToPromise = (
uploadRequest: ReturnType<typeof got.stream.post>,
logger: Logger,
) =>
new Promise<{ buildId: number }>(function (resolve, reject) {
uploadRequest.on('error', reject).on('data', function handleMessage(data) {
let obj;
@ -106,10 +109,7 @@ const uploadToPromise = (uploadRequest: Request, logger: Logger) =>
});
});
/**
* @returns {Promise<{ buildId: number }>}
*/
const uploadImage = function (
const uploadImage = async function (
imageStream: NodeJS.ReadableStream & { length: number },
token: string,
username: string,
@ -117,10 +117,9 @@ const uploadImage = function (
appName: string,
logger: Logger,
): Promise<{ buildId: number }> {
const request = require('request') as typeof import('request');
const progressStream =
require('progress-stream') as typeof import('progress-stream');
const zlib = require('zlib') as typeof import('zlib');
const { default: got } = await import('got');
const progressStream = await import('progress-stream');
const zlib = await import('zlib');
// Need to strip off the newline
const progressMessage = logger
@ -141,25 +140,26 @@ const uploadImage = function (
),
);
const uploadRequest = request.post({
url: getBuilderPushEndpoint(url, username, appName),
headers: {
'Content-Encoding': 'gzip',
const uploadRequest = got.stream.post(
getBuilderPushEndpoint(url, username, appName),
{
headers: {
'Content-Encoding': 'gzip',
Authorization: `Bearer ${token}`,
},
body: streamWithProgress.pipe(
zlib.createGzip({
level: 6,
}),
),
throwHttpErrors: false,
},
auth: {
bearer: token,
},
body: streamWithProgress.pipe(
zlib.createGzip({
level: 6,
}),
),
});
);
return uploadToPromise(uploadRequest, logger);
};
const uploadLogs = function (
const uploadLogs = async function (
logs: string,
token: string,
url: string,
@ -167,14 +167,14 @@ const uploadLogs = function (
username: string,
appName: string,
) {
const request = require('request') as typeof import('request');
return request.post({
json: true,
url: getBuilderLogPushEndpoint(url, buildId, username, appName),
auth: {
bearer: token,
const { default: got } = await import('got');
return got.post(getBuilderLogPushEndpoint(url, buildId, username, appName), {
headers: {
Authorization: `Bearer ${token}`,
},
body: Buffer.from(logs),
responseType: 'json',
throwHttpErrors: false,
});
};
@ -232,7 +232,7 @@ export const deployLegacy = async function (
username,
appName,
]);
uploadLogs(...args);
await uploadLogs(...args);
}
return buildId;

View File

@ -94,7 +94,7 @@ async function installQemu(arch: string, qemuPath: string) {
const urlVersion = encodeURIComponent(QEMU_VERSION);
const qemuUrl = `https://github.com/balena-io/qemu/releases/download/${urlVersion}/${urlFile}`;
const request = await import('request');
const { default: got } = await import('got');
const fs = await import('fs');
const zlib = await import('zlib');
const tar = await import('tar-stream');
@ -117,7 +117,8 @@ async function installQemu(arch: string, qemuPath: string) {
reject(err as Error);
}
});
request(qemuUrl)
got.stream
.get(qemuUrl)
.on('error', reject)
.pipe(zlib.createGunzip())
.on('error', reject)

View File

@ -16,7 +16,8 @@ limitations under the License.
import type { BalenaSDK } from 'balena-sdk';
import * as JSONStream from 'JSONStream';
import * as readline from 'readline';
import * as request from 'request';
import type { PlainResponse } from 'got';
import type got from 'got';
import type { RegistrySecrets } from '@balena/compose/dist/multibuild';
import type * as Stream from 'stream';
import streamToPromise = require('stream-to-promise');
@ -119,7 +120,7 @@ export async function startRemoteBuild(
} catch (err) {
console.error(err.message);
} finally {
buildRequest.abort();
buildRequest.destroy();
const sigintErr = new SIGINTError('Build aborted on SIGINT signal');
sigintErr.code = 'SIGINT';
stream.emit('error', sigintErr);
@ -337,32 +338,29 @@ async function getTarStream(build: RemoteBuild): Promise<Stream.Readable> {
/**
* Initiate a POST HTTP request to the remote builder and add some event
* listeners.
*
* ¡! Note: this function must be synchronous because of a bug in the `request`
* library that requires the following two steps to take place in the same
* iteration of Node's event loop: (1) adding a listener for the 'response'
* event and (2) calling request.pipe():
* https://github.com/request/request/issues/887
*/
function createRemoteBuildRequest(
async function createRemoteBuildRequest(
build: RemoteBuild,
tarStream: Stream.Readable,
builderUrl: string,
onError: (error: Error) => void,
): request.Request {
const zlib = require('zlib') as typeof import('zlib');
) {
const { default: got } = await import('got');
const zlib = await import('zlib');
if (DEBUG_MODE) {
console.error(`[debug] Connecting to builder at ${builderUrl}`);
}
return request
.post({
url: builderUrl,
auth: { bearer: build.auth },
headers: { 'Content-Encoding': 'gzip' },
return got.stream
.post(builderUrl, {
headers: {
Authorization: `Bearer ${build.auth}`,
'Content-Encoding': 'gzip',
},
body: tarStream.pipe(zlib.createGzip({ level: 6 })),
throwHttpErrors: false,
})
.once('error', onError) // `.once` because the handler re-emits
.once('response', (response: request.RequestResponse) => {
.once('response', (response: PlainResponse) => {
if (response.statusCode >= 100 && response.statusCode < 400) {
if (DEBUG_MODE) {
console.error(
@ -374,8 +372,8 @@ function createRemoteBuildRequest(
'Remote builder responded with HTTP error:',
`${response.statusCode} ${response.statusMessage}`,
];
if (response.body) {
msgArr.push(response.body);
if (response.rawBody) {
msgArr.push(response.rawBody.toString());
}
onError(new ExpectedError(msgArr.join('\n')));
}
@ -384,7 +382,7 @@ function createRemoteBuildRequest(
async function getRemoteBuildStream(
build: RemoteBuild,
): Promise<[request.Request, Stream.Stream]> {
): Promise<[ReturnType<typeof got.stream.post>, Stream.Stream]> {
const builderUrl = await getBuilderEndpoint(
build.baseUrl,
build.appSlug,
@ -412,7 +410,7 @@ async function getRemoteBuildStream(
}
const tarStream = await getTarStream(build);
const buildRequest = createRemoteBuildRequest(
const buildRequest = await createRemoteBuildRequest(
build,
tarStream,
builderUrl,

View File

@ -20,7 +20,7 @@ import * as chaiAsPromised from 'chai-as-promised';
import * as ejs from 'ejs';
import * as fs from 'fs';
import * as path from 'path';
import * as request from 'request';
import got from 'got';
import * as sinon from 'sinon';
import { LoginServer } from '../../build/auth/server';
@ -61,38 +61,30 @@ describe('Login server:', function () {
server.shutdown();
});
async function testLogin(opt: {
async function testLogin({
verb = 'post',
...opt
}: {
expectedBody: string;
expectedErrorMsg?: string;
expectedStatusCode: number;
expectedToken: string;
urlPath?: string;
verb?: string;
verb?: 'post' | 'put';
}) {
opt.urlPath = opt.urlPath ?? addr.urlPath;
const post = opt.verb
? ((request as any)[opt.verb] as typeof request.post)
: request.post;
await new Promise<void>((resolve, reject) => {
post(
`http://${addr.host}:${addr.port}${opt.urlPath}`,
{
form: {
token: opt.expectedToken,
},
const res = await got[verb](
`http://${addr.host}:${addr.port}${opt.urlPath}`,
{
form: {
token: opt.expectedToken,
},
function (error, response, body) {
try {
expect(error).to.not.exist;
expect(response.statusCode).to.equal(opt.expectedStatusCode);
expect(body).to.equal(opt.expectedBody);
resolve();
} catch (err) {
reject(err as Error);
}
},
);
});
throwHttpErrors: false,
},
);
expect(res.body).to.equal(opt.expectedBody);
expect(res.statusCode).to.equal(opt.expectedStatusCode);
try {
const token = await server.awaitForToken();
@ -127,7 +119,7 @@ describe('Login server:', function () {
expectedStatusCode: 404,
expectedToken: tokens.johndoe.token,
expectedErrorMsg: 'Unknown path or verb',
verb: 'get',
verb: 'put',
});
});

View File

@ -442,6 +442,93 @@ describe('balena build', function () {
});
});
it('should create the expected tar stream (docker-compose --nologs)', async () => {
const projectPath = path.join(projectsPath, 'docker-compose', 'basic');
const service1Dockerfile = (
await fs.readFile(
path.join(projectPath, 'service1', 'Dockerfile.template'),
'utf8',
)
).replace('%%BALENA_MACHINE_NAME%%', 'nuc');
const expectedFilesByService: ExpectedTarStreamFilesByService = {
service1: {
Dockerfile: {
contents: service1Dockerfile,
fileSize: service1Dockerfile.length,
type: 'file',
},
'Dockerfile.template': { fileSize: 144, type: 'file' },
'file1.sh': { fileSize: 12, type: 'file' },
},
service2: {
'Dockerfile-alt': { fileSize: 13, type: 'file' },
'file2-crlf.sh': {
fileSize: isWindows ? 12 : 14,
testStream: isWindows ? expectStreamNoCRLF : undefined,
type: 'file',
},
'src/file1.sh': { fileSize: 12, type: 'file' },
},
};
const responseFilename = 'build-POST.json';
const responseBody = await fs.readFile(
path.join(dockerResponsePath, responseFilename),
'utf8',
);
const expectedQueryParamsByService = {
service1: Object.entries(
_.merge({}, commonComposeQueryParams, {
buildargs: {
COMPOSE_ARG: 'A',
barg: 'b',
SERVICE1_VAR: 'This is a service specific variable',
},
cachefrom: ['my/img1', 'my/img2'],
}),
),
service2: Object.entries(
_.merge({}, commonComposeQueryParamsIntel, {
buildargs: {
COMPOSE_ARG: 'A',
barg: 'b',
},
cachefrom: ['my/img1', 'my/img2'],
dockerfile: 'Dockerfile-alt',
}),
),
};
const expectedResponseLines: string[] = [
...commonResponseLines[responseFilename],
...getDockerignoreWarn1(
[path.join(projectPath, 'service2', '.dockerignore')],
'build',
),
];
if (isWindows) {
expectedResponseLines.push(
`[Info] Converting line endings CRLF -> LF for file: ${path.join(
projectPath,
'service2',
'file2-crlf.sh',
)}`,
);
}
docker.expectGetInfo({});
docker.expectGetManifestNucAlpine();
docker.expectGetManifestBusybox();
await testDockerBuildStream({
commandLine: `build ${projectPath} --deviceType nuc --arch amd64 -B COMPOSE_ARG=A -B barg=b --cache-from my/img1,my/img2 --nologs`,
dockerMock: docker,
expectedFilesByService,
expectedQueryParamsByService,
expectedResponseLines,
projectPath,
responseBody,
responseCode: 200,
services: ['service1', 'service2'],
});
});
it('should create the expected tar stream (docker-compose, --multi-dockerignore)', async () => {
const projectPath = path.join(projectsPath, 'docker-compose', 'basic');
const service1Dockerfile = (

View File

@ -27,7 +27,7 @@ import * as sinon from 'sinon';
import { BalenaAPIMock } from '../nock/balena-api-mock';
import { expectStreamNoCRLF, testDockerBuildStream } from '../docker-build';
import { DockerMock, dockerResponsePath } from '../nock/docker-mock';
import { cleanOutput, runCommand, switchSentry } from '../helpers';
import { cleanOutput, runCommand } from '../helpers';
import type {
ExpectedTarStreamFiles,
ExpectedTarStreamFilesByService,
@ -262,7 +262,6 @@ describe('balena deploy', function () {
});
it('should update a release with status="failed" on error (single container)', async () => {
let sentryStatus: boolean | undefined;
const projectPath = path.join(projectsPath, 'no-docker-compose', 'basic');
const expectedFiles: ExpectedTarStreamFiles = {
'src/.dockerignore': { fileSize: 16, type: 'file' },
@ -319,7 +318,6 @@ describe('balena deploy', function () {
api.expectPostImageLabel();
try {
sentryStatus = await switchSentry(false);
sinon.stub(process, 'exit');
await testDockerBuildStream({
@ -337,9 +335,8 @@ describe('balena deploy', function () {
});
expect(failedImagePatchRequests).to.equal(maxRequestRetries);
} finally {
await switchSentry(sentryStatus);
// @ts-expect-error claims restore does not exist
process.exit.restore();
// We mock process.exit and need to force cast it to a SinonStub to restore it
(process.exit as unknown as sinon.SinonStub).restore();
}
});

View File

@ -417,15 +417,3 @@ export function deepJsonParse(data: any): any {
}
return data;
}
export async function switchSentry(
enabled: boolean | undefined,
): Promise<boolean | undefined> {
const balenaCLI = await import('../build/app');
const sentryOpts = (await balenaCLI.setupSentry()).getClient()?.getOptions();
if (sentryOpts) {
const sentryStatus = sentryOpts.enabled;
sentryOpts.enabled = enabled;
return sentryStatus;
}
}

View File

@ -41,23 +41,18 @@ export class BuilderMock extends NockMock {
checkBuildRequestBody: (requestBody: string | Buffer) => Promise<void>;
}) {
this.optPost(/^\/v3\/build($|[(?])/, opts).reply(
async function (uri, requestBody, callback) {
let error: Error | null = null;
try {
await opts.checkURI(uri);
if (typeof requestBody === 'string') {
const gzipped = Buffer.from(requestBody, 'hex');
const gunzipped = await gunzipAsync(gzipped);
await opts.checkBuildRequestBody(gunzipped);
} else {
throw new Error(
`unexpected requestBody type "${typeof requestBody}"`,
);
}
} catch (err) {
error = err;
async function (uri, requestBody) {
await opts.checkURI(uri);
if (typeof requestBody === 'string') {
const gzipped = Buffer.from(requestBody, 'hex');
const gunzipped = await gunzipAsync(gzipped);
await opts.checkBuildRequestBody(gunzipped);
return [opts.responseCode, opts.responseBody];
} else {
throw new Error(
`unexpected requestBody type "${typeof requestBody}"`,
);
}
callback(error, [opts.responseCode, opts.responseBody]);
},
);
}

View File

@ -81,21 +81,14 @@ export class DockerMock extends NockMock {
this.optPost(
new RegExp(`^/build\\?(|.+&)${qs.stringify({ t: opts.tag })}&`),
opts,
).reply(async function (uri, requestBody, cb) {
let error: Error | null = null;
try {
await opts.checkURI(uri);
if (typeof requestBody === 'string') {
await opts.checkBuildRequestBody(requestBody);
} else {
throw new Error(
`unexpected requestBody type "${typeof requestBody}"`,
);
}
} catch (err) {
error = err;
).reply(async function (uri, requestBody) {
await opts.checkURI(uri);
if (typeof requestBody === 'string') {
await opts.checkBuildRequestBody(requestBody);
return [opts.responseCode, opts.responseBody];
} else {
throw new Error(`unexpected requestBody type "${typeof requestBody}"`);
}
cb(error, [opts.responseCode, opts.responseBody]);
});
}

View File

@ -63,7 +63,7 @@ describe('detectEncoding() function', function () {
it('should correctly detect the encoding of a few selected files', async () => {
const sampleBinary = [
'drivelist/build/Release/drivelist.node',
'mountutils/build/Release/MountUtils.node',
'mountutils/prebuilds/linux-x64/mountutils.node',
];
const sampleText = [
'node_modules/.bin/mocha',