balena-cli 23.2.15-build-update-compose-7-3-0-97bf26975807627a264274621ffcf8380aea838e-1 → 23.2.15-build-mockttp-6cb0abaec2656b73d1230e92f9e8a907f95bef98-1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "balena-cli",
3
- "version": "23.2.15-build-update-compose-7-3-0-97bf26975807627a264274621ffcf8380aea838e-1",
3
+ "version": "23.2.15-build-mockttp-6cb0abaec2656b73d1230e92f9e8a907f95bef98-1",
4
4
  "description": "The official balena Command Line Interface",
5
5
  "main": "./build/app.js",
6
6
  "homepage": "https://github.com/balena-io/balena-cli",
@@ -165,6 +165,7 @@
165
165
  "mocha": "^10.6.0",
166
166
  "mock-fs": "^5.2.0",
167
167
  "mock-require": "^3.0.3",
168
+ "mockttp": "^4.2.0",
168
169
  "nock": "^14.0.4",
169
170
  "oclif": "^4.22.61",
170
171
  "rewire": "^7.0.0",
@@ -173,10 +174,11 @@
173
174
  "sinon": "^19.0.0",
174
175
  "string-to-stream": "^3.0.1",
175
176
  "ts-node": "^10.4.0",
176
- "typescript": "^5.9.2"
177
+ "typescript": "^5.9.2",
178
+ "undici": "^7.16.0"
177
179
  },
178
180
  "dependencies": {
179
- "@balena/compose": "^7.3.0",
181
+ "@balena/compose": "^7.0.10",
180
182
  "@balena/dockerignore": "^1.0.2",
181
183
  "@balena/env-parsing": "^1.1.8",
182
184
  "@balena/es-version": "^1.0.1",
@@ -262,6 +264,6 @@
262
264
  "balena-request": "14.0.6"
263
265
  },
264
266
  "versionist": {
265
- "publishedAt": "2026-01-05T23:04:56.483Z"
267
+ "publishedAt": "2026-01-07T17:29:28.041Z"
266
268
  }
267
269
  }
@@ -208,7 +208,7 @@ export default class OsConfigureCmd extends Command {
208
208
  })) as DeviceWithDeviceType & {
209
209
  belongs_to__application: BalenaSdk.PineDeferred;
210
210
  };
211
- deviceTypeSlug = device.is_of__device_type[0].slug!;
211
+ deviceTypeSlug = device.is_of__device_type[0].slug;
212
212
  } else if (fleetSlugOrId != null) {
213
213
  app = await getApplication(balena, fleetSlugOrId, {
214
214
  $select: 'slug',
@@ -226,7 +226,7 @@ export default class OsConfigureCmd extends Command {
226
226
 
227
227
  const deviceTypeManifest = await helpers.getManifest(
228
228
  params.image,
229
- deviceTypeSlug!,
229
+ deviceTypeSlug,
230
230
  );
231
231
 
232
232
  const { normalizeOsVersion } = await import('../../utils/normalization');
@@ -242,7 +242,7 @@ export default class OsConfigureCmd extends Command {
242
242
  );
243
243
  await validateSecureBootOptionAndWarn(
244
244
  secureBoot,
245
- deviceTypeSlug!,
245
+ deviceTypeSlug,
246
246
  osVersion,
247
247
  );
248
248
 
@@ -258,7 +258,7 @@ export default class OsConfigureCmd extends Command {
258
258
  };
259
259
  const answers: AugmentedAnswers = {
260
260
  ...baseAnswers,
261
- deviceType: deviceTypeSlug!,
261
+ deviceType: deviceTypeSlug,
262
262
  version: osVersion,
263
263
  developmentMode: developmentMode,
264
264
  secureBoot: secureBoot,
@@ -89,10 +89,10 @@ export default class ReleaseListCmd extends Command {
89
89
  (r) => r.id,
90
90
  );
91
91
 
92
- const augmentedReleases = releases.map((release) => {
93
- const extra = releasesWithExplicitReadFieldsById[release.id];
94
- return typeof extra === 'object' ? { ...release, ...extra } : release;
95
- });
92
+ const augmentedReleases = releases.map((release) => ({
93
+ ...release,
94
+ ...releasesWithExplicitReadFieldsById[release.id],
95
+ }));
96
96
 
97
97
  await pipeline(
98
98
  Readable.from(augmentedReleases),
@@ -89,7 +89,10 @@ export class DeprecationChecker {
89
89
  * @param version Semver without 'v' prefix, e.g. '12.0.0.'
90
90
  */
91
91
  protected getNpmUrl(version: string) {
92
- return `https://registry.npmjs.org/balena-cli/${version}`;
92
+ // Allow override for testing with mock servers
93
+ const registryUrl =
94
+ process.env.BALENARC_NPM_REGISTRY ?? 'https://registry.npmjs.org';
95
+ return `${registryUrl}/balena-cli/${version}`;
93
96
  }
94
97
 
95
98
  /**
@@ -15,7 +15,10 @@
15
15
  * limitations under the License.
16
16
  */
17
17
 
18
- import type { BalenaModel } from 'balena-sdk';
18
+ import type {
19
+ ImageModel,
20
+ ReleaseModel,
21
+ } from '@balena/compose/dist/release/models';
19
22
  import type { Composition, ImageDescriptor } from '@balena/compose/dist/parse';
20
23
  import type { Pack } from 'tar-stream';
21
24
 
@@ -24,32 +27,25 @@ interface Image {
24
27
  tag: string;
25
28
  }
26
29
 
27
- interface ImageProperties {
28
- dockerfile?: string;
29
- projectType?: string;
30
- size?: number;
31
- startTime?: Date;
32
- endTime?: Date;
33
- }
34
-
35
30
  export interface BuiltImage {
36
31
  logs: string;
37
32
  name: string;
38
- props: ImageProperties;
33
+ props: {
34
+ dockerfile?: string;
35
+ projectType?: string;
36
+ size?: number;
37
+ startTime?: Date;
38
+ endTime?: Date;
39
+ };
39
40
  serviceName: string;
40
41
  }
41
42
 
42
- type ServiceImage = Omit<
43
- BalenaModel['image']['Read'],
44
- 'created_at' | 'is_a_build_of__service'
45
- >;
46
-
47
43
  export interface TaggedImage {
48
44
  localImage: import('dockerode').Image;
49
- serviceImage: ServiceImage;
45
+ serviceImage: import('@balena/compose/dist/release/models').ImageModel;
50
46
  serviceName: string;
51
47
  logs: string;
52
- props: ImageProperties;
48
+ props: BuiltImage.props;
53
49
  registry: string;
54
50
  repo: string;
55
51
  }
@@ -86,32 +82,23 @@ export interface ComposeProject {
86
82
  export interface Release {
87
83
  client: import('@balena/compose').release.Request['client'];
88
84
  release: Pick<
89
- BalenaModel['release']['Read'],
85
+ ReleaseModel,
90
86
  | 'id'
91
87
  | 'status'
92
88
  | 'commit'
93
89
  | 'composition'
94
90
  | 'source'
95
91
  | 'is_final'
92
+ | 'contract'
96
93
  | 'semver'
97
94
  | 'start_timestamp'
98
95
  | 'end_timestamp'
99
- > & {
100
- contract: Contract | null;
101
- };
102
- serviceImages: Dictionary<ServiceImage>;
96
+ >;
97
+ serviceImages: Dictionary<
98
+ Omit<ImageModel, 'created_at' | 'is_a_build_of__service'>
99
+ >;
103
100
  }
104
101
 
105
- // TODO: The balena contract model for release.contract is `{ [key: string]: JSON } | JSON[] | null`
106
- // which appears to be incorrect, as a contract is represented as `{ [key: string]: any }`
107
- // (more specifically, `{ type: string, [key: string]: any }`) when querying a release for its contract.
108
- // @balena/contrato also defines a ContractObject as the type below, see:
109
- // https://github.com/balena-io/contrato/blob/543e891249431aa98474e17fecc66617ab9ca8f3/lib/types.ts#L15-L17
110
- export type Contract = {
111
- type: string;
112
- [key: string]: any;
113
- };
114
-
115
102
  interface TarDirectoryOptions {
116
103
  composition?: Composition;
117
104
  convertEol?: boolean;
@@ -26,7 +26,6 @@ import type {
26
26
  ComposeOpts,
27
27
  ComposeProject,
28
28
  Release,
29
- Contract,
30
29
  TaggedImage,
31
30
  } from './compose-types';
32
31
  import Logger = require('./logger');
@@ -129,7 +128,7 @@ export const createRelease = async function (
129
128
  composition: Composition,
130
129
  draft: boolean,
131
130
  semver: string | undefined,
132
- contract: Contract | undefined,
131
+ contract: import('@balena/compose/dist/release/models').ReleaseModel['contract'],
133
132
  imgDescriptors: ImageDescriptor[],
134
133
  ): Promise<Release> {
135
134
  const crypto = require('crypto') as typeof import('crypto');
@@ -166,18 +165,16 @@ export const createRelease = async function (
166
165
  commit: crypto.pseudoRandomBytes(16).toString('hex').toLowerCase(),
167
166
  semver,
168
167
  is_final: !draft,
169
- contract: contract ?? undefined,
168
+ contract,
170
169
  imgDescriptors,
171
170
  });
172
171
 
173
172
  for (const serviceImage of Object.values(serviceImages)) {
174
173
  if ('created_at' in serviceImage) {
175
- // created_at is not an optional field in the SDK, but we need to delete it
176
- serviceImage.created_at = undefined as any;
174
+ delete serviceImage.created_at;
177
175
  }
178
176
  if ('is_a_build_of__service' in serviceImage) {
179
- // is_a_build_of__service is not an optional field in the SDK, but we need to delete it
180
- serviceImage.is_a_build_of__service = undefined as any;
177
+ delete serviceImage.is_a_build_of__service;
181
178
  }
182
179
  }
183
180
 
@@ -194,7 +191,7 @@ export const createRelease = async function (
194
191
  'semver',
195
192
  'start_timestamp',
196
193
  'end_timestamp',
197
- ]) as Release['release'],
194
+ ]),
198
195
  serviceImages,
199
196
  };
200
197
  };
@@ -39,8 +39,6 @@ import type {
39
39
  ComposeProject,
40
40
  TaggedImage,
41
41
  TarDirectoryOptions,
42
- Release,
43
- Contract,
44
42
  } from './compose-types';
45
43
  import type { DeviceInfo } from './device/api';
46
44
  import { getBalenaSdk, getCliUx, stripIndent } from './lazy';
@@ -1276,7 +1274,7 @@ async function pushAndUpdateServiceImages(
1276
1274
  token: string,
1277
1275
  images: TaggedImage[],
1278
1276
  afterEach: (
1279
- serviceImage: TaggedImage['serviceImage'],
1277
+ serviceImage: import('@balena/compose/dist/release/models').ImageModel,
1280
1278
  props: object,
1281
1279
  ) => Promise<void>,
1282
1280
  ) {
@@ -1332,15 +1330,15 @@ async function pushAndUpdateServiceImages(
1332
1330
  serviceImage.image_size = `${imgInfo.Size}`;
1333
1331
  serviceImage.content_hash = imgDigest;
1334
1332
  serviceImage.build_log = logs;
1335
- serviceImage.dockerfile = props.dockerfile ?? null;
1336
- serviceImage.project_type = props.projectType ?? null;
1333
+ serviceImage.dockerfile = props.dockerfile;
1334
+ serviceImage.project_type = props.projectType;
1337
1335
  if (props.startTime) {
1338
- serviceImage.start_timestamp = props.startTime.toISOString();
1336
+ serviceImage.start_timestamp = props.startTime;
1339
1337
  }
1340
1338
  if (props.endTime) {
1341
- serviceImage.end_timestamp = props.endTime.toISOString();
1339
+ serviceImage.end_timestamp = props.endTime;
1342
1340
  }
1343
- serviceImage.push_timestamp = new Date().toISOString();
1341
+ serviceImage.push_timestamp = new Date();
1344
1342
  serviceImage.status = 'success';
1345
1343
  } catch (error) {
1346
1344
  serviceImage.error_message = '' + error;
@@ -1381,7 +1379,7 @@ async function pushServiceImages(
1381
1379
  `Saving image ${serviceImage.is_stored_at__image_location}`,
1382
1380
  );
1383
1381
  if (skipLogUpload) {
1384
- serviceImage.build_log = null;
1382
+ delete serviceImage.build_log;
1385
1383
  }
1386
1384
 
1387
1385
  // These are the only update-able image fields in bC atm, and passing
@@ -1427,7 +1425,7 @@ export async function deployProject(
1427
1425
  projectPath: string,
1428
1426
  isDraft: boolean,
1429
1427
  imgDescriptors: ImageDescriptor[],
1430
- ): Promise<Release['release']> {
1428
+ ): Promise<import('@balena/compose/dist/release/models').ReleaseModel> {
1431
1429
  const releaseMod = await import('@balena/compose/dist/release');
1432
1430
  const { createRelease, tagServiceImages } = await import('./compose');
1433
1431
  const tty = (await import('./tty'))(process.stdout);
@@ -1498,7 +1496,7 @@ export async function deployProject(
1498
1496
  }
1499
1497
  } finally {
1500
1498
  await runSpinner(tty, spinner, `${prefix}Saving release...`, async () => {
1501
- release.end_timestamp = new Date().toISOString();
1499
+ release.end_timestamp = new Date();
1502
1500
  if (release.id != null) {
1503
1501
  await releaseMod.updateRelease(pineClient, release.id, {
1504
1502
  status: release.status,
@@ -1554,7 +1552,9 @@ export function createRunLoop(tick: (...args: any[]) => void) {
1554
1552
 
1555
1553
  async function getContractContent(
1556
1554
  filePath: string,
1557
- ): Promise<Contract | undefined> {
1555
+ ): Promise<
1556
+ import('@balena/compose/dist/release/models').ReleaseModel['contract']
1557
+ > {
1558
1558
  let fileContentAsString;
1559
1559
  try {
1560
1560
  fileContentAsString = await fs.readFile(filePath, 'utf8');
@@ -1584,7 +1584,7 @@ async function getContractContent(
1584
1584
  return asJson;
1585
1585
  }
1586
1586
 
1587
- function isContract(obj: any): obj is Contract {
1587
+ function isContract(obj: any): obj is Dictionary<any> {
1588
1588
  return obj?.type && allowedContractTypes.includes(obj.type);
1589
1589
  }
1590
1590
 
@@ -74,7 +74,9 @@ export class DeviceAPI {
74
74
  addr: string,
75
75
  port = 48484,
76
76
  ) {
77
- this.deviceAddress = `http://${addr}:${port}/`;
77
+ // Allow override for testing with mock servers
78
+ this.deviceAddress =
79
+ process.env.BALENARC_SUPERVISOR_ADDRESS ?? `http://${addr}:${port}/`;
78
80
  }
79
81
 
80
82
  // Either return nothing, or throw an error with the info