Compare commits

...

31 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
ebfabdba6a v22.0.1 2025-05-28 17:00:48 +00:00
b0cbe43708 Merge pull request #2947 from balena-io/nock-14
Update `nock` to 14.0.4 & Move DeviceAPI off `request`
2025-05-28 13:59:58 -03:00
b7f1469912 Deduplicate dependencies 2025-05-28 12:08:37 -04:00
3396ba5a97 DeviceAPI: Move away from request in favor of BalenaSdk request
Change-type: patch
2025-05-28 12:08:37 -04:00
78ffff83bc Update nock to 14.0.4
Change-type: patch
2025-05-28 08:35:40 -04:00
24 changed files with 1777 additions and 1312 deletions

View File

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

View File

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

View File

@ -1,3 +1,212 @@
- 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
body: ""
footer:
Change-type: patch
change-type: patch
author: myarmolinsky
nested: []
- subject: Update `nock` to 14.0.4
hash: 78ffff83bca1d87dff19909d39d3d18815754a0e
body: ""
footer:
Change-type: patch
change-type: patch
author: myarmolinsky
nested: []
version: 22.0.1
title: ""
date: 2025-05-28T17:00:45.889Z
- commits: - commits:
- subject: Add migration guide to v22 - subject: Add migration guide to v22
hash: 45eb0ad4b145d1eed2c30bcd1f9bc9e1a9a2d719 hash: 45eb0ad4b145d1eed2c30bcd1f9bc9e1a9a2d719

View File

@ -4,6 +4,72 @@ All notable changes to this project will be documented in this file
automatically by Versionist. DO NOT EDIT THIS FILE MANUALLY! automatically by Versionist. DO NOT EDIT THIS FILE MANUALLY!
This project adheres to [Semantic Versioning](http://semver.org/). 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]
* Update `nock` to 14.0.4 [myarmolinsky]
## 22.0.0 - 2025-05-26 ## 22.0.0 - 2025-05-26
* Add migration guide to v22 [Otavio Jacobi] * Add migration guide to v22 [Otavio Jacobi]

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

2238
npm-shrinkwrap.json generated

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

@ -38,11 +38,11 @@ import { stripIndent } from './utils/lazy';
export async function trackCommand(commandSignature: string) { export async function trackCommand(commandSignature: string) {
try { try {
let Sentry: typeof import('@sentry/node'); let Sentry: typeof import('@sentry/node');
let scope: import('@sentry/node').Scope;
if (!process.env.BALENARC_NO_SENTRY) { if (!process.env.BALENARC_NO_SENTRY) {
Sentry = await import('@sentry/node'); Sentry = await import('@sentry/node');
Sentry.configureScope((scope) => { scope = Sentry.getCurrentScope();
scope.setExtra('command', commandSignature); scope.setExtra('command', commandSignature);
});
} }
const { getCachedUsername } = await import('./utils/bootstrap'); const { getCachedUsername } = await import('./utils/bootstrap');
let username: string | undefined; let username: string | undefined;
@ -52,11 +52,9 @@ export async function trackCommand(commandSignature: string) {
// ignore // ignore
} }
if (!process.env.BALENARC_NO_SENTRY) { if (!process.env.BALENARC_NO_SENTRY) {
Sentry!.configureScope((scope) => { scope!.setUser({
scope.setUser({ id: username,
id: username, username,
username,
});
}); });
} }
if ( 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( async function pushServiceImages(
docker: Dockerode, docker: Dockerode,
logger: Logger, logger: Logger,
@ -1344,23 +1347,34 @@ async function pushServiceImages(
delete serviceImage.build_log; delete serviceImage.build_log;
} }
await releaseMod.updateImage( // These are the only update-able image fields in bC atm, and passing
pineClient, // the whole image object in v7+ would result the allowlist to reject the request.
serviceImage.id, const imagePayload = _.pick(serviceImage, [
// These are the only update-able image fields in bC atm, and passing 'end_timestamp',
// the whole image object in v7+ would result the allowlist to reject the request. 'project_type',
_.pick(serviceImage, [ 'error_message',
'end_timestamp', 'build_log',
'project_type', 'push_timestamp',
'error_message', 'status',
'build_log', 'content_hash',
'push_timestamp', 'dockerfile',
'status', 'image_size',
'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; 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); const match = stepRegex.exec(str);
if (match) { if (match) {
step = match[1]; step = match[1];

View File

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

View File

@ -15,12 +15,12 @@
* limitations under the License. * limitations under the License.
*/ */
import * as _ from 'lodash'; import * as _ from 'lodash';
import * as request from 'request';
import type * as Stream from 'stream';
import { retry } from '../helpers'; import { retry } from '../helpers';
import Logger = require('../logger'); import Logger = require('../logger');
import * as ApiErrors from './errors'; import * as ApiErrors from './errors';
import { getBalenaSdk } from '../lazy';
import type { BalenaSDK } from 'balena-sdk';
export interface DeviceResponse { export interface DeviceResponse {
[key: string]: any; [key: string]: any;
@ -80,9 +80,9 @@ export class DeviceAPI {
} }
// Either return nothing, or throw an error with the info // Either return nothing, or throw an error with the info
public async setTargetState(state: any): Promise<void> { public async setTargetState(state: Record<string, any>) {
const url = this.getUrlForAction('setTargetState'); const url = this.getUrlForAction('setTargetState');
return DeviceAPI.promisifiedRequest( await DeviceAPI.sendRequest(
{ {
method: 'POST', method: 'POST',
url, url,
@ -96,37 +96,37 @@ export class DeviceAPI {
public async getTargetState() { public async getTargetState() {
const url = this.getUrlForAction('getTargetState'); const url = this.getUrlForAction('getTargetState');
return DeviceAPI.promisifiedRequest( return await DeviceAPI.sendRequest(
{ {
method: 'GET', method: 'GET',
url, url,
json: true, json: true,
}, },
this.logger, this.logger,
).then((body) => { ).then(({ state }: { state: Record<string, any> }) => {
return body.state; return state;
}); });
} }
public async getDeviceInformation(): Promise<DeviceInfo> { public async getDeviceInformation() {
const url = this.getUrlForAction('getDeviceInformation'); const url = this.getUrlForAction('getDeviceInformation');
return DeviceAPI.promisifiedRequest( return await DeviceAPI.sendRequest(
{ {
method: 'GET', method: 'GET',
url, url,
json: true, json: true,
}, },
this.logger, this.logger,
).then((body) => { ).then(({ info }: { info: DeviceInfo }) => {
return body.info; return info;
}); });
} }
public async getContainerId(serviceName: string): Promise<string> { public async getContainerId(serviceName: string): Promise<string> {
const url = this.getUrlForAction('containerId'); const url = this.getUrlForAction('containerId');
const body = await DeviceAPI.promisifiedRequest( const body = await DeviceAPI.sendRequest(
{ {
method: 'GET', method: 'GET',
url, url,
@ -146,10 +146,10 @@ export class DeviceAPI {
return body.containerId; return body.containerId;
} }
public async ping(): Promise<void> { public async ping() {
const url = this.getUrlForAction('ping'); const url = this.getUrlForAction('ping');
return DeviceAPI.promisifiedRequest( await DeviceAPI.sendRequest(
{ {
method: 'GET', method: 'GET',
url, url,
@ -158,10 +158,10 @@ export class DeviceAPI {
); );
} }
public getVersion(): Promise<string> { public async getVersion(): Promise<string> {
const url = this.getUrlForAction('version'); const url = this.getUrlForAction('version');
return DeviceAPI.promisifiedRequest({ return await DeviceAPI.sendRequest({
method: 'GET', method: 'GET',
url, url,
json: true, json: true,
@ -176,10 +176,10 @@ export class DeviceAPI {
}); });
} }
public getStatus(): Promise<Status> { public async getStatus() {
const url = this.getUrlForAction('status'); const url = this.getUrlForAction('status');
return DeviceAPI.promisifiedRequest({ return await DeviceAPI.sendRequest({
method: 'GET', method: 'GET',
url, url,
json: true, json: true,
@ -194,96 +194,60 @@ export class DeviceAPI {
}); });
} }
public getLogStream(): Promise<Stream.Readable> { public async getLogStream() {
const url = this.getUrlForAction('logs'); const url = this.getUrlForAction('logs');
const sdk = getBalenaSdk();
// Don't use the promisified version here as we want to stream the output const stream = await sdk.request.stream({ url });
return new Promise((resolve, reject) => { stream.on('response', (res) => {
const req = request.get(url); if (res.statusCode !== 200) {
throw new ApiErrors.DeviceAPIError(
req.on('error', reject).on('response', (res) => { 'Non-200 response from log streaming endpoint',
if (res.statusCode !== 200) { );
reject( }
new ApiErrors.DeviceAPIError( res.socket.setKeepAlive(true, 1000);
'Non-200 response from log streaming endpoint',
),
);
return;
}
try {
res.socket.setKeepAlive(true, 1000);
} catch (error) {
reject(error as Error);
}
resolve(res);
});
}); });
return stream;
} }
private getUrlForAction(action: keyof typeof deviceEndpoints): string { private getUrlForAction(action: keyof typeof deviceEndpoints) {
return `${this.deviceAddress}${deviceEndpoints[action]}`; return `${this.deviceAddress}${deviceEndpoints[action]}`;
} }
// A helper method for promisifying general (non-streaming) requests. Streaming // A helper method for promisifying general (non-streaming) requests. Streaming
// requests should use a seperate setup // requests should use a seperate setup
private static async promisifiedRequest< private static async sendRequest(
T extends Parameters<typeof request>[0], opts: Parameters<BalenaSDK['request']['send']>[number],
>(opts: T, logger?: Logger): Promise<any> { logger?: Logger,
interface ObjectWithUrl { ) {
url?: string; if (logger != null && opts.url != null) {
} logger.logDebug(`Sending request to ${opts.url}`);
if (logger != null) {
let url: string | null = null;
if (_.isObject(opts) && (opts as ObjectWithUrl).url != null) {
// the `as string` shouldn't be necessary, but the type system
// is getting a little confused
url = (opts as ObjectWithUrl).url!;
} else if (typeof opts === 'string') {
url = opts;
}
if (url != null) {
logger.logDebug(`Sending request to ${url}`);
}
} }
const sdk = getBalenaSdk();
const doRequest = async () => { const doRequest = async () => {
return await new Promise((resolve, reject) => { const response = await sdk.request.send(opts);
return request(opts, (err, response, body) => { const bodyError =
if (err) { typeof response.body === 'string'
reject(err as Error); ? response.body
return; : response.body.message;
} switch (response.statusCode) {
switch (response.statusCode) { case 200:
case 200: { return response.body;
resolve(body); case 400:
return; throw new ApiErrors.BadRequestDeviceAPIError(bodyError);
} case 503:
case 400: { throw new ApiErrors.ServiceUnavailableAPIError(bodyError);
reject(new ApiErrors.BadRequestDeviceAPIError(body.message)); default:
return; new ApiErrors.DeviceAPIError(bodyError);
} }
case 503: {
reject(new ApiErrors.ServiceUnavailableAPIError(body.message));
return;
}
default: {
reject(new ApiErrors.DeviceAPIError(body.message));
return;
}
}
});
});
}; };
return await retry({ return await retry({
func: doRequest, func: doRequest,
initialDelayMs: 2000, initialDelayMs: 2000,
maxAttempts: 6, maxAttempts: 6,
label: `Supervisor API (${opts.method} ${(opts as any).url})`, label: `Supervisor API (${opts.method} ${opts.url})`,
}); });
} }
} }
export default DeviceAPI;

View File

@ -603,11 +603,11 @@ function getImageNameFromTask(task: BuildTask): string {
} }
export function generateTargetState( export function generateTargetState(
currentTargetState: any, currentTargetState: Record<string, any>,
composition: Composition, composition: Composition,
buildTasks: BuildTask[], buildTasks: BuildTask[],
env: ParsedEnvironment, env: ParsedEnvironment,
): any { ) {
const keyedBuildTasks = _.keyBy(buildTasks, 'serviceName'); const keyedBuildTasks = _.keyBy(buildTasks, 'serviceName');
const services: { [serviceId: string]: any } = {}; const services: { [serviceId: string]: any } = {};

View File

@ -28,7 +28,7 @@ import { instanceOf } from '../../errors';
import Logger = require('../logger'); import Logger = require('../logger');
import { Dockerfile } from 'livepush'; import { Dockerfile } from 'livepush';
import type DeviceAPI from './api'; import type { DeviceAPI } from './api';
import type { DeviceInfo, Status } from './api'; import type { DeviceInfo, Status } from './api';
import type { DeviceDeployOptions } from './deploy'; import type { DeviceDeployOptions } from './deploy';
import { generateTargetState, rebuildSingleTask } from './deploy'; import { generateTargetState, rebuildSingleTask } from './deploy';

View File

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

View File

@ -16,7 +16,8 @@ limitations under the License.
import type { BalenaSDK } from 'balena-sdk'; import type { BalenaSDK } from 'balena-sdk';
import * as JSONStream from 'JSONStream'; import * as JSONStream from 'JSONStream';
import * as readline from 'readline'; 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 { RegistrySecrets } from '@balena/compose/dist/multibuild';
import type * as Stream from 'stream'; import type * as Stream from 'stream';
import streamToPromise = require('stream-to-promise'); import streamToPromise = require('stream-to-promise');
@ -119,7 +120,7 @@ export async function startRemoteBuild(
} catch (err) { } catch (err) {
console.error(err.message); console.error(err.message);
} finally { } finally {
buildRequest.abort(); buildRequest.destroy();
const sigintErr = new SIGINTError('Build aborted on SIGINT signal'); const sigintErr = new SIGINTError('Build aborted on SIGINT signal');
sigintErr.code = 'SIGINT'; sigintErr.code = 'SIGINT';
stream.emit('error', sigintErr); 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 * Initiate a POST HTTP request to the remote builder and add some event
* listeners. * 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, build: RemoteBuild,
tarStream: Stream.Readable, tarStream: Stream.Readable,
builderUrl: string, builderUrl: string,
onError: (error: Error) => void, 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) { if (DEBUG_MODE) {
console.error(`[debug] Connecting to builder at ${builderUrl}`); console.error(`[debug] Connecting to builder at ${builderUrl}`);
} }
return request return got.stream
.post({ .post(builderUrl, {
url: builderUrl, headers: {
auth: { bearer: build.auth }, Authorization: `Bearer ${build.auth}`,
headers: { 'Content-Encoding': 'gzip' }, 'Content-Encoding': 'gzip',
},
body: tarStream.pipe(zlib.createGzip({ level: 6 })), body: tarStream.pipe(zlib.createGzip({ level: 6 })),
throwHttpErrors: false,
}) })
.once('error', onError) // `.once` because the handler re-emits .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 (response.statusCode >= 100 && response.statusCode < 400) {
if (DEBUG_MODE) { if (DEBUG_MODE) {
console.error( console.error(
@ -374,8 +372,8 @@ function createRemoteBuildRequest(
'Remote builder responded with HTTP error:', 'Remote builder responded with HTTP error:',
`${response.statusCode} ${response.statusMessage}`, `${response.statusCode} ${response.statusMessage}`,
]; ];
if (response.body) { if (response.rawBody) {
msgArr.push(response.body); msgArr.push(response.rawBody.toString());
} }
onError(new ExpectedError(msgArr.join('\n'))); onError(new ExpectedError(msgArr.join('\n')));
} }
@ -384,7 +382,7 @@ function createRemoteBuildRequest(
async function getRemoteBuildStream( async function getRemoteBuildStream(
build: RemoteBuild, build: RemoteBuild,
): Promise<[request.Request, Stream.Stream]> { ): Promise<[ReturnType<typeof got.stream.post>, Stream.Stream]> {
const builderUrl = await getBuilderEndpoint( const builderUrl = await getBuilderEndpoint(
build.baseUrl, build.baseUrl,
build.appSlug, build.appSlug,
@ -412,7 +410,7 @@ async function getRemoteBuildStream(
} }
const tarStream = await getTarStream(build); const tarStream = await getTarStream(build);
const buildRequest = createRemoteBuildRequest( const buildRequest = await createRemoteBuildRequest(
build, build,
tarStream, tarStream,
builderUrl, builderUrl,

View File

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

View File

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

View File

@ -21,8 +21,6 @@ import { BalenaAPIMock } from '../../nock/balena-api-mock';
import { cleanOutput, runCommand } from '../../helpers'; import { cleanOutput, runCommand } from '../../helpers';
import { SupervisorMock } from '../../nock/supervisor-mock'; import { SupervisorMock } from '../../nock/supervisor-mock';
const itS = process.env.BALENA_CLI_TEST_TYPE === 'standalone' ? it : it.skip;
describe('balena device logs', function () { describe('balena device logs', function () {
let api: BalenaAPIMock; let api: BalenaAPIMock;
let supervisor: SupervisorMock; let supervisor: SupervisorMock;
@ -39,10 +37,7 @@ describe('balena device logs', function () {
supervisor.done(); supervisor.done();
}); });
// skip non-standalone tests because nock's mock socket causes the error: it('should reach the expected endpoints on a local device', async () => {
// "setKeepAliveInterval expects an instance of socket as its first argument"
// in utils/device/api.ts: NetKeepalive.setKeepAliveInterval(sock, 5000);
itS('should reach the expected endpoints on a local device', async () => {
supervisor.expectGetPing(); supervisor.expectGetPing();
supervisor.expectGetLogs(); supervisor.expectGetLogs();
supervisor.expectGetLogs(); supervisor.expectGetLogs();

View File

@ -417,15 +417,3 @@ export function deepJsonParse(data: any): any {
} }
return data; 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>; checkBuildRequestBody: (requestBody: string | Buffer) => Promise<void>;
}) { }) {
this.optPost(/^\/v3\/build($|[(?])/, opts).reply( this.optPost(/^\/v3\/build($|[(?])/, opts).reply(
async function (uri, requestBody, callback) { async function (uri, requestBody) {
let error: Error | null = null; await opts.checkURI(uri);
try { if (typeof requestBody === 'string') {
await opts.checkURI(uri); const gzipped = Buffer.from(requestBody, 'hex');
if (typeof requestBody === 'string') { const gunzipped = await gunzipAsync(gzipped);
const gzipped = Buffer.from(requestBody, 'hex'); await opts.checkBuildRequestBody(gunzipped);
const gunzipped = await gunzipAsync(gzipped); return [opts.responseCode, opts.responseBody];
await opts.checkBuildRequestBody(gunzipped); } else {
} else { throw new Error(
throw new Error( `unexpected requestBody type "${typeof requestBody}"`,
`unexpected requestBody type "${typeof requestBody}"`, );
);
}
} catch (err) {
error = err;
} }
callback(error, [opts.responseCode, opts.responseBody]);
}, },
); );
} }

View File

@ -81,21 +81,14 @@ export class DockerMock extends NockMock {
this.optPost( this.optPost(
new RegExp(`^/build\\?(|.+&)${qs.stringify({ t: opts.tag })}&`), new RegExp(`^/build\\?(|.+&)${qs.stringify({ t: opts.tag })}&`),
opts, opts,
).reply(async function (uri, requestBody, cb) { ).reply(async function (uri, requestBody) {
let error: Error | null = null; await opts.checkURI(uri);
try { if (typeof requestBody === 'string') {
await opts.checkURI(uri); await opts.checkBuildRequestBody(requestBody);
if (typeof requestBody === 'string') { return [opts.responseCode, opts.responseBody];
await opts.checkBuildRequestBody(requestBody); } else {
} else { throw new Error(`unexpected requestBody type "${typeof requestBody}"`);
throw new Error(
`unexpected requestBody type "${typeof requestBody}"`,
);
}
} catch (err) {
error = err;
} }
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 () => { it('should correctly detect the encoding of a few selected files', async () => {
const sampleBinary = [ const sampleBinary = [
'drivelist/build/Release/drivelist.node', 'drivelist/build/Release/drivelist.node',
'mountutils/build/Release/MountUtils.node', 'mountutils/prebuilds/linux-x64/mountutils.node',
]; ];
const sampleText = [ const sampleText = [
'node_modules/.bin/mocha', 'node_modules/.bin/mocha',