Compare commits

..

2 Commits

Author SHA1 Message Date
2b76a3999f Deduplicate dependencies 2025-06-02 15:31:14 +03:00
ebbdb6f96a Update nock to 15.0.0-beta.3 to work with native fetch
Nock 15 adds support for native fetch.

Change-type: patch
See: https://github.com/nock/nock/blob/beta/migration_guides/migrating_to_15.md
2025-06-02 15:30:34 +03:00
16 changed files with 616 additions and 478 deletions

View File

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

View File

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

View File

@ -1,54 +1,3 @@
- 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

View File

@ -4,24 +4,6 @@ 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]

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 23 and later are not yet fully supported.**
> **The balena CLI currently requires Node.js version ^20.6.0**
> **Versions 21 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 22
$ nvm install 20
```
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 22
$ nvm install 20
```
#### **Windows** (not WSL)
@ -113,7 +113,7 @@ $ nvm install 22
Install:
* If you'd like the ability to switch between Node.js versions, install
- Node.js v22 from the [Nodejs.org releases page](https://nodejs.org/en/download/releases/).
- Node.js v20 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:

808
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.1.1",
"version": "22.0.6",
"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 <23"
"node": "^20.6.0"
},
"oclif": {
"bin": "balena",
@ -153,7 +153,7 @@
"mocha": "^10.6.0",
"mock-fs": "^5.2.0",
"mock-require": "^3.0.3",
"nock": "^14.0.4",
"nock": "^15.0.0-beta.3",
"oclif": "^4.17.0",
"rewire": "^7.0.0",
"simple-git": "^3.14.1",
@ -170,7 +170,7 @@
"@oclif/core": "^4.1.0",
"@sentry/node": "^9.0.0",
"balena-config-json": "^4.2.7",
"balena-device-init": "^8.1.11",
"balena-device-init": "^8.1.3",
"balena-errors": "^4.7.3",
"balena-image-fs": "^7.5.2",
"balena-preload": "^18.0.4",
@ -191,7 +191,7 @@
"docker-progress": "^5.1.3",
"dockerode": "^4.0.5",
"ejs": "^3.1.6",
"etcher-sdk": "^10.0.0",
"etcher-sdk": "^9.1.4",
"express": "^4.17.2",
"fast-boot2": "^1.1.0",
"fast-levenshtein": "^3.0.0",
@ -219,8 +219,8 @@
"prettyjson": "^1.2.5",
"progress-stream": "^2.0.0",
"reconfix": "^1.0.0-v0-1-0-fork-46760acff4d165f5238bfac5e464256ef1944476",
"resin-cli-form": "^4.0.0",
"resin-cli-visuals": "^3.0.0",
"resin-cli-form": "^3.0.0",
"resin-cli-visuals": "^2.0.1",
"resin-doodles": "^0.2.0",
"resin-stream-logger": "^0.1.2",
"rimraf": "^3.0.2",
@ -248,6 +248,6 @@
}
},
"versionist": {
"publishedAt": "2025-06-19T09:32:53.877Z"
"publishedAt": "2025-06-02T12:27:11.982Z"
}
}

View File

@ -1322,9 +1322,6 @@ 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,
@ -1347,34 +1344,23 @@ async function pushServiceImages(
delete serviceImage.build_log;
}
// 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);
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',
]),
);
},
);
}

View File

@ -108,7 +108,7 @@ describe('Utils:', async function () {
});
});
return describe('given the token does authenticate with the server', function () {
describe('given the token does authenticate with the server', function () {
beforeEach(function () {
this.balenaAuthIsLoggedInStub = sinon.stub(balena.auth, 'isLoggedIn');
return this.balenaAuthIsLoggedInStub.resolves(true);
@ -118,7 +118,7 @@ describe('Utils:', async function () {
return this.balenaAuthIsLoggedInStub.restore();
});
return it('should eventually be true', function () {
it('should eventually be true', function () {
const promise = utils.loginIfTokenValid(tokens.johndoe.token);
return expect(promise).to.eventually.be.true;
});

View File

@ -20,7 +20,6 @@ import type { Request as ReleaseRequest } from '@balena/compose/dist/release';
import { expect } from 'chai';
import { promises as fs } from 'fs';
import * as _ from 'lodash';
import type * as nock from 'nock';
import * as path from 'path';
import * as sinon from 'sinon';
@ -181,8 +180,8 @@ describe('balena deploy', function () {
];
api.expectPostRelease({
inspectRequest: (_uri: string, requestBody: nock.Body) => {
const body = requestBody.valueOf() as Partial<ReleaseRequest>;
inspectRequest: async (request: Request) => {
const body = (await request.json()) as Partial<ReleaseRequest>;
expect(body.contract).to.deep.equal({
name: 'testContract',
type: 'sw.application',
@ -232,8 +231,8 @@ describe('balena deploy', function () {
];
api.expectPostRelease({
inspectRequest: (_uri: string, requestBody: nock.Body) => {
const body = requestBody.valueOf() as Partial<ReleaseRequest>;
inspectRequest: async (request: Request) => {
const body = (await request.json()) as Partial<ReleaseRequest>;
expect(body.contract).to.deep.equal({
name: 'testContract',
type: 'sw.application',
@ -298,21 +297,21 @@ describe('balena deploy', function () {
statusCode: 500,
// b/c failed requests are retried
times: maxRequestRetries,
inspectRequest: (_uri, requestBody) => {
const imageBody = requestBody as Partial<
inspectRequest: async (request: Request) => {
const body = (await request.json()) as Partial<
import('@balena/compose/dist/release/models').ImageModel
>;
expect(imageBody.status).to.equal('success');
expect(body.status).to.equal('success');
failedImagePatchRequests++;
},
});
// Check that the CLI patches the release with status="failed"
api.expectPatchRelease({
inspectRequest: (_uri, requestBody) => {
const releaseBody = requestBody as Partial<
inspectRequest: async (request: Request) => {
const body = (await request.json()) as Partial<
import('@balena/compose/dist/release/models').ReleaseModel
>;
expect(releaseBody.status).to.equal('failed');
expect(body.status).to.equal('failed');
},
});
api.expectPostImageLabel();
@ -386,8 +385,8 @@ describe('balena deploy', function () {
let succesfullImagePatchRequests = 0;
api
.optPatch(/^\/v7\/image($|[(?])/, { times: maxRequestRetries })
.reply((_uri, requestBody) => {
const imageBody = requestBody as Partial<
.reply(async (request) => {
const imageBody = (await request.json()) as Partial<
import('@balena/compose/dist/release/models').ImageModel
>;
expect(imageBody.status).to.equal('success');

View File

@ -61,12 +61,15 @@ export class BalenaAPIMock extends NockMock {
}
public expectDownloadConfig(opts: ScopeOpts = {}) {
this.optPost('/download-config', opts).reply(200, (_uri, body) => {
let deviceType = 'raspberrypi3';
if (typeof body === 'object' && 'deviceType' in body) {
deviceType = body.deviceType;
}
return JSON.parse(`{
this.optPost('/download-config', opts).reply(
200,
async (request: Request) => {
const body = await request.json();
let deviceType = 'raspberrypi3';
if (typeof body === 'object' && 'deviceType' in body) {
deviceType = body.deviceType;
}
return JSON.parse(`{
"applicationId":1301645,
"deviceType":"${deviceType}",
"userId":43699,
@ -79,7 +82,8 @@ export class BalenaAPIMock extends NockMock {
"deltaEndpoint":"https://delta.balena-cloud.com",
"apiKey":"nothingtoseehere"
}`);
});
},
);
}
public expectApplicationProvisioning(opts: ScopeOpts = {}) {
@ -284,8 +288,8 @@ export class BalenaAPIMock extends NockMock {
public expectGetAppServiceVars(opts: ScopeOpts = {}) {
this.optGet(/^\/v\d+\/service_environment_variable($|\?)/, opts).reply(
function (uri, _requestBody) {
const match = uri.match(/service_name%20eq%20%27(.+?)%27/);
function (request) {
const match = request.url.match(/service_name%20eq%20%27(.+?)%27/);
const serviceName = match?.[1] || undefined;
let varArray: any[];
if (serviceName) {
@ -332,8 +336,8 @@ export class BalenaAPIMock extends NockMock {
this.optGet(
/^\/v\d+\/device_service_environment_variable($|\?)/,
opts,
).reply(function (uri, _requestBody) {
const match = uri.match(/service_name%20eq%20%27(.+?)%27/);
).reply(function (request) {
const match = request.url.match(/service_name%20eq%20%27(.+?)%27/);
const serviceName = match?.[1] || undefined;
let varArray: any[];
if (serviceName) {

View File

@ -16,12 +16,7 @@
*/
import * as path from 'path';
import * as zlib from 'zlib';
import { NockMock } from './nock-mock';
import { promisify } from 'util';
const gunzipAsync = promisify(zlib.gunzip);
export const builderResponsePath = path.normalize(
path.join(__dirname, '..', 'test-data', 'builder-response'),
@ -40,20 +35,17 @@ export class BuilderMock extends NockMock {
checkURI: (uri: string) => Promise<void> | void;
checkBuildRequestBody: (requestBody: string | Buffer) => Promise<void>;
}) {
this.optPost(/^\/v3\/build($|[(?])/, opts).reply(
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}"`,
);
}
},
);
this.optPost(/^\/v3\/build($|[(?])/, opts).reply(async function (
request: Request,
) {
await opts.checkURI(request.url);
const requestBody = await request.text();
if (typeof requestBody === 'string') {
await opts.checkBuildRequestBody(requestBody);
} else {
throw new Error(`unexpected requestBody type "${typeof requestBody}"`);
}
return [opts.responseCode, opts.responseBody];
});
}
}

View File

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

View File

@ -100,47 +100,28 @@ export class NockMock {
return this.optMethod('post', uri, opts);
}
protected inspectNoOp(_uri: string, _requestBody: nock.Body): void {
return undefined;
protected inspectNoOp(_request: Request): void | Promise<void> {
return;
}
protected getInspectedReplyBodyFunction(
inspectRequest: (uri: string, requestBody: nock.Body) => void,
inspectRequest: (request: Request) => void | Promise<void>,
replyBody: nock.ReplyBody,
) {
return function (
this: nock.ReplyFnContext,
uri: string,
requestBody: nock.Body,
cb: (err: NodeJS.ErrnoException | null, result: nock.ReplyBody) => void,
) {
try {
inspectRequest(uri, requestBody);
} catch (err) {
cb(err, '');
}
cb(null, replyBody);
return async function (request: Request): Promise<nock.ReplyBody> {
await inspectRequest(request);
return replyBody;
};
}
protected getInspectedReplyFileFunction(
inspectRequest: (uri: string, requestBody: nock.Body) => void,
inspectRequest: (request: Request) => void | Promise<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, '');
}
return async function (request: Request): Promise<nock.ReplyBody> {
await inspectRequest(request);
const replyBody = fs.readFileSync(replyBodyFile);
cb(null, replyBody);
return replyBody;
};
}

View File

@ -48,8 +48,8 @@ export class SupervisorMock extends NockMock {
}, 10);
},
});
this.optGet('/v2/local/logs', opts).reply((_uri, _reqBody, cb) => {
cb(null, [200, chunkedStream]);
this.optGet('/v2/local/logs', opts).reply(() => {
return [200, chunkedStream];
});
}
}

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/prebuilds/linux-x64/mountutils.node',
'mountutils/build/Release/MountUtils.node',
];
const sampleText = [
'node_modules/.bin/mocha',