balena-cli 23.2.14 → 23.2.15-build-update-compose-7-3-0-97bf26975807627a264274621ffcf8380aea838e-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.14",
3
+ "version": "23.2.15-build-update-compose-7-3-0-97bf26975807627a264274621ffcf8380aea838e-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",
@@ -176,7 +176,7 @@
176
176
  "typescript": "^5.9.2"
177
177
  },
178
178
  "dependencies": {
179
- "@balena/compose": "^7.0.10",
179
+ "@balena/compose": "^7.3.0",
180
180
  "@balena/dockerignore": "^1.0.2",
181
181
  "@balena/env-parsing": "^1.1.8",
182
182
  "@balena/es-version": "^1.0.1",
@@ -262,6 +262,6 @@
262
262
  "balena-request": "14.0.6"
263
263
  },
264
264
  "versionist": {
265
- "publishedAt": "2025-12-30T11:51:18.913Z"
265
+ "publishedAt": "2026-01-05T23:04:56.483Z"
266
266
  }
267
267
  }
@@ -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
- ...release,
94
- ...releasesWithExplicitReadFieldsById[release.id],
95
- }));
92
+ const augmentedReleases = releases.map((release) => {
93
+ const extra = releasesWithExplicitReadFieldsById[release.id];
94
+ return typeof extra === 'object' ? { ...release, ...extra } : release;
95
+ });
96
96
 
97
97
  await pipeline(
98
98
  Readable.from(augmentedReleases),
@@ -15,10 +15,7 @@
15
15
  * limitations under the License.
16
16
  */
17
17
 
18
- import type {
19
- ImageModel,
20
- ReleaseModel,
21
- } from '@balena/compose/dist/release/models';
18
+ import type { BalenaModel } from 'balena-sdk';
22
19
  import type { Composition, ImageDescriptor } from '@balena/compose/dist/parse';
23
20
  import type { Pack } from 'tar-stream';
24
21
 
@@ -27,25 +24,32 @@ interface Image {
27
24
  tag: string;
28
25
  }
29
26
 
27
+ interface ImageProperties {
28
+ dockerfile?: string;
29
+ projectType?: string;
30
+ size?: number;
31
+ startTime?: Date;
32
+ endTime?: Date;
33
+ }
34
+
30
35
  export interface BuiltImage {
31
36
  logs: string;
32
37
  name: string;
33
- props: {
34
- dockerfile?: string;
35
- projectType?: string;
36
- size?: number;
37
- startTime?: Date;
38
- endTime?: Date;
39
- };
38
+ props: ImageProperties;
40
39
  serviceName: string;
41
40
  }
42
41
 
42
+ type ServiceImage = Omit<
43
+ BalenaModel['image']['Read'],
44
+ 'created_at' | 'is_a_build_of__service'
45
+ >;
46
+
43
47
  export interface TaggedImage {
44
48
  localImage: import('dockerode').Image;
45
- serviceImage: import('@balena/compose/dist/release/models').ImageModel;
49
+ serviceImage: ServiceImage;
46
50
  serviceName: string;
47
51
  logs: string;
48
- props: BuiltImage.props;
52
+ props: ImageProperties;
49
53
  registry: string;
50
54
  repo: string;
51
55
  }
@@ -82,23 +86,32 @@ export interface ComposeProject {
82
86
  export interface Release {
83
87
  client: import('@balena/compose').release.Request['client'];
84
88
  release: Pick<
85
- ReleaseModel,
89
+ BalenaModel['release']['Read'],
86
90
  | 'id'
87
91
  | 'status'
88
92
  | 'commit'
89
93
  | 'composition'
90
94
  | 'source'
91
95
  | 'is_final'
92
- | 'contract'
93
96
  | 'semver'
94
97
  | 'start_timestamp'
95
98
  | 'end_timestamp'
96
- >;
97
- serviceImages: Dictionary<
98
- Omit<ImageModel, 'created_at' | 'is_a_build_of__service'>
99
- >;
99
+ > & {
100
+ contract: Contract | null;
101
+ };
102
+ serviceImages: Dictionary<ServiceImage>;
100
103
  }
101
104
 
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
+
102
115
  interface TarDirectoryOptions {
103
116
  composition?: Composition;
104
117
  convertEol?: boolean;
@@ -26,6 +26,7 @@ import type {
26
26
  ComposeOpts,
27
27
  ComposeProject,
28
28
  Release,
29
+ Contract,
29
30
  TaggedImage,
30
31
  } from './compose-types';
31
32
  import Logger = require('./logger');
@@ -128,7 +129,7 @@ export const createRelease = async function (
128
129
  composition: Composition,
129
130
  draft: boolean,
130
131
  semver: string | undefined,
131
- contract: import('@balena/compose/dist/release/models').ReleaseModel['contract'],
132
+ contract: Contract | undefined,
132
133
  imgDescriptors: ImageDescriptor[],
133
134
  ): Promise<Release> {
134
135
  const crypto = require('crypto') as typeof import('crypto');
@@ -165,16 +166,18 @@ export const createRelease = async function (
165
166
  commit: crypto.pseudoRandomBytes(16).toString('hex').toLowerCase(),
166
167
  semver,
167
168
  is_final: !draft,
168
- contract,
169
+ contract: contract ?? undefined,
169
170
  imgDescriptors,
170
171
  });
171
172
 
172
173
  for (const serviceImage of Object.values(serviceImages)) {
173
174
  if ('created_at' in serviceImage) {
174
- delete serviceImage.created_at;
175
+ // created_at is not an optional field in the SDK, but we need to delete it
176
+ serviceImage.created_at = undefined as any;
175
177
  }
176
178
  if ('is_a_build_of__service' in serviceImage) {
177
- delete serviceImage.is_a_build_of__service;
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;
178
181
  }
179
182
  }
180
183
 
@@ -191,7 +194,7 @@ export const createRelease = async function (
191
194
  'semver',
192
195
  'start_timestamp',
193
196
  'end_timestamp',
194
- ]),
197
+ ]) as Release['release'],
195
198
  serviceImages,
196
199
  };
197
200
  };
@@ -39,6 +39,8 @@ import type {
39
39
  ComposeProject,
40
40
  TaggedImage,
41
41
  TarDirectoryOptions,
42
+ Release,
43
+ Contract,
42
44
  } from './compose-types';
43
45
  import type { DeviceInfo } from './device/api';
44
46
  import { getBalenaSdk, getCliUx, stripIndent } from './lazy';
@@ -1274,7 +1276,7 @@ async function pushAndUpdateServiceImages(
1274
1276
  token: string,
1275
1277
  images: TaggedImage[],
1276
1278
  afterEach: (
1277
- serviceImage: import('@balena/compose/dist/release/models').ImageModel,
1279
+ serviceImage: TaggedImage['serviceImage'],
1278
1280
  props: object,
1279
1281
  ) => Promise<void>,
1280
1282
  ) {
@@ -1330,15 +1332,15 @@ async function pushAndUpdateServiceImages(
1330
1332
  serviceImage.image_size = `${imgInfo.Size}`;
1331
1333
  serviceImage.content_hash = imgDigest;
1332
1334
  serviceImage.build_log = logs;
1333
- serviceImage.dockerfile = props.dockerfile;
1334
- serviceImage.project_type = props.projectType;
1335
+ serviceImage.dockerfile = props.dockerfile ?? null;
1336
+ serviceImage.project_type = props.projectType ?? null;
1335
1337
  if (props.startTime) {
1336
- serviceImage.start_timestamp = props.startTime;
1338
+ serviceImage.start_timestamp = props.startTime.toISOString();
1337
1339
  }
1338
1340
  if (props.endTime) {
1339
- serviceImage.end_timestamp = props.endTime;
1341
+ serviceImage.end_timestamp = props.endTime.toISOString();
1340
1342
  }
1341
- serviceImage.push_timestamp = new Date();
1343
+ serviceImage.push_timestamp = new Date().toISOString();
1342
1344
  serviceImage.status = 'success';
1343
1345
  } catch (error) {
1344
1346
  serviceImage.error_message = '' + error;
@@ -1379,7 +1381,7 @@ async function pushServiceImages(
1379
1381
  `Saving image ${serviceImage.is_stored_at__image_location}`,
1380
1382
  );
1381
1383
  if (skipLogUpload) {
1382
- delete serviceImage.build_log;
1384
+ serviceImage.build_log = null;
1383
1385
  }
1384
1386
 
1385
1387
  // These are the only update-able image fields in bC atm, and passing
@@ -1425,7 +1427,7 @@ export async function deployProject(
1425
1427
  projectPath: string,
1426
1428
  isDraft: boolean,
1427
1429
  imgDescriptors: ImageDescriptor[],
1428
- ): Promise<import('@balena/compose/dist/release/models').ReleaseModel> {
1430
+ ): Promise<Release['release']> {
1429
1431
  const releaseMod = await import('@balena/compose/dist/release');
1430
1432
  const { createRelease, tagServiceImages } = await import('./compose');
1431
1433
  const tty = (await import('./tty'))(process.stdout);
@@ -1496,7 +1498,7 @@ export async function deployProject(
1496
1498
  }
1497
1499
  } finally {
1498
1500
  await runSpinner(tty, spinner, `${prefix}Saving release...`, async () => {
1499
- release.end_timestamp = new Date();
1501
+ release.end_timestamp = new Date().toISOString();
1500
1502
  if (release.id != null) {
1501
1503
  await releaseMod.updateRelease(pineClient, release.id, {
1502
1504
  status: release.status,
@@ -1552,9 +1554,7 @@ export function createRunLoop(tick: (...args: any[]) => void) {
1552
1554
 
1553
1555
  async function getContractContent(
1554
1556
  filePath: string,
1555
- ): Promise<
1556
- import('@balena/compose/dist/release/models').ReleaseModel['contract']
1557
- > {
1557
+ ): Promise<Contract | undefined> {
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 Dictionary<any> {
1587
+ function isContract(obj: any): obj is Contract {
1588
1588
  return obj?.type && allowedContractTypes.includes(obj.type);
1589
1589
  }
1590
1590