Compare commits

...

92 Commits

Author SHA1 Message Date
776115ef5d v21.1.6 2025-04-03 14:12:08 +00:00
f031ec1dea Merge pull request #2931 from balena-io/renovate/actions-setup-node-digest
Update actions/setup-node digest to cdca736
2025-04-03 14:11:10 +00:00
fe42438090 Update actions/setup-node digest to cdca736
Update actions/setup-node

Change-type: patch
2025-04-03 13:50:06 +00:00
b616fbdd79 v21.1.5 2025-04-03 13:28:12 +00:00
81edfbbae1 Merge pull request #2930 from balena-io/bump_dockerode_cachefrom
Update dockerode/docker-modem dependencies for cachefrom fix
2025-04-03 09:27:11 -04:00
663e83c3b8 Dedupe dependencies 2025-04-02 18:50:31 -04:00
b650f8ff6d Update dockerode/docker-modem dependencies for fixes
Change-type: patch
Signed-off-by: Ken Bannister <kb2ma@runbox.com>
2025-04-02 18:47:43 -04:00
58234f17e1 v21.1.4 2025-04-02 09:16:30 +00:00
77905f4a74 Merge pull request #2928 from balena-io/comment_sig_file
Add comment with example of signature file in a flasher image
2025-04-02 09:15:23 +00:00
30076fabe6 Dedupe dependencies 2025-04-01 18:25:20 -04:00
28703bb5ae Add comment with secure boot signature file example for preload
Change-type: patch
Signed-off-by: Ken Bannister <kb2ma@runbox.com>
2025-04-01 18:25:16 -04:00
37b3c6abe9 v21.1.3 2025-03-28 16:57:03 +00:00
b4e473e4d4 Merge pull request #2927 from balena-io/fix-ob-device-detail
Fix device detail for open balena
2025-03-28 16:55:56 +00:00
0d4e411777 Fix device detail for open balena
Change-type: patch
2025-03-28 13:29:52 -03:00
7e6f2189e8 v21.1.2 2025-03-27 12:20:26 +00:00
3903daf8a8 Merge pull request #2914 from balena-io/deny-preload-sb
Deny preload for an image with secure boot enabled
2025-03-27 08:19:14 -04:00
18bc0d61e7 Dedupe dependencies 2025-03-26 22:55:54 -04:00
7f2daeebb0 Deny preload for an image with secure boot enabled
Change-type: patch
Signed-off-by: Ken Bannister <kb2ma@runbox.com>
2025-03-26 22:40:43 -04:00
813e9cb82e v21.1.1 2025-03-26 20:34:47 +00:00
3bcb3c1b2e Merge pull request #2925 from balena-io/add-overall-status
Add overall status
2025-03-26 20:33:52 +00:00
20d76556c2 Add explicit read properties for device command
Change-tye: minor
2025-03-26 17:12:45 -03:00
e829068725 Bump balena-sdk to 21.3.0
Update balena-sdk from 21.2.1 to 21.3.0

Change-type: patch
2025-03-26 17:07:20 -03:00
650e896f70 v21.1.0 2025-03-12 19:34:20 +00:00
a9042124ea Merge pull request #2922 from balena-io/compose-requirement-labels
Add support for requirement labels feature
2025-03-12 19:33:23 +00:00
d24d78dac7 Fix package release action for macOS
Depending on the signing password value, the script may interpret the
contents of the password and cause the signing process to fail.

This puts quotes around the password on assignment to prevent this.
2025-03-12 15:30:49 -03:00
42c50ef8ae Add support for new requirement labels feature
Updates @balena/compose to v7 to include this new feature.

See: https://balena.fibery.io/Work/Project/Refactoring-container-contracts-1205
Depends-on: https://github.com/balena-io-modules/balena-compose/pull/64
Change-type: minor
2025-03-12 15:30:21 -03:00
ba4b9bd447 v21.0.0 2025-03-11 14:42:31 +00:00
02c0ea5b59 Merge pull request #2921 from balena-io/major-21-second-attempt
Major 21
2025-03-11 10:41:28 -04:00
bc3558dd8e Address SDK major v21 breaking changes 2025-03-11 08:19:10 -04:00
aad62d1ccd Drop support for OS versions <2.14.0
Change-type: major
2025-03-11 08:19:10 -04:00
ecc6f80164 api-key generate: Add required argument expiryDate
Change-type: major
2025-03-11 08:19:10 -04:00
c0fd1e3886 Deduplicate dependencies 2025-03-11 08:18:56 -04:00
9d3120b144 Update balena-preload to 18.0.1
Change-type: patch
2025-03-11 08:17:27 -04:00
ed0e03ddb2 Add dependency date-fns
Change-type: patch
2025-03-11 08:16:50 -04:00
8fe6d6c026 Update balena-sdk to 21.2.1
Change-type: patch
2025-03-11 08:16:31 -04:00
727033ae14 v20.2.10 2025-03-10 17:33:13 +00:00
c19ce6a905 Merge pull request #2923 from balena-io/bump-typescript-to-5.8.2
Bump typescript to 5.8.2
2025-03-10 17:32:13 +00:00
1a33029738 Deduplicate dependencies 2025-03-10 17:51:18 +02:00
043bc48a1c Add a "deduplicate-dependencies" npm script to standardize such commits 2025-03-04 08:48:31 +02:00
a10156a441 Update TypeScript to 5.8.2
Change-type: patch
2025-03-04 08:48:31 +02:00
4f665f43d2 v20.2.9 2025-02-26 12:52:10 +00:00
9f097a96f5 Merge pull request #2920 from balena-io/x-balena-client-fix
Fix CORS issue with X-Balena-Client header
2025-02-26 12:51:20 +00:00
64d1943804 Fix CORS issue with X-Balena-Client header
Change-type: patch
See: https://balena.fibery.io/Work/Project/Extend-the-X-Balena-Client-header-to-include-the-UI-CLI-version-as-well-1174
2025-02-26 14:24:57 +02:00
666ce876e6 v20.2.8 2025-02-26 00:22:16 +00:00
e01184080f Merge pull request #2915 from balena-io/fix_os_configure_test
Update balena-config-json dependency and fix test
2025-02-25 19:21:25 -05:00
93039b010d Update balena-config-json dependency and fix test
Change-type: patch
Signed-off-by: Ken Bannister <kb2ma@runbox.com>
2025-02-25 18:43:12 -05:00
795259bf30 v20.2.7 2025-02-25 20:21:03 +00:00
fa134d2d39 Merge pull request #2917 from balena-io/x-balena-client
Use the CLI version in the X-Balena-Client header
2025-02-25 20:20:10 +00:00
bef5221ed8 Use the CLI version in the X-Balena-Client header
Change-type: patch
See: https://balena.fibery.io/Work/Project/Extend-the-X-Balena-Client-header-to-include-the-UI-CLI-version-as-well-1174
2025-02-25 21:59:35 +02:00
72d6db796c v20.2.6 2025-02-25 19:13:51 +00:00
e848eb63ee Merge pull request #2918 from balena-io/renovate/actions-upload-artifact-digest
Update actions/upload-artifact digest to 4cec3d8
2025-02-25 19:12:56 +00:00
6f0f7350cf Update actions/upload-artifact digest to 4cec3d8
Update actions/upload-artifact

Change-type: patch
2025-02-25 18:51:27 +00:00
07a88c700e v20.2.5 2025-02-25 18:10:50 +00:00
9cae66bd92 Merge pull request #2913 from balena-io/renovate/actions-setup-node-digest
Update actions/setup-node digest to 1d0ff46
2025-02-25 18:09:54 +00:00
cddea24cef Update actions/setup-node digest to 1d0ff46
Update actions/setup-node

Change-type: patch
2025-02-25 17:45:52 +00:00
b1c246c0b4 v20.2.4 2025-02-25 17:17:03 +00:00
00b4d57a03 Merge pull request #2916 from balena-io/pin_docker-modem_regression
Pin docker-modem and dockerode to avoid regression in docker-modem v5.0.6
2025-02-25 12:16:10 -05:00
2cba82e914 Pin docker-modem and dockerode to avoid regression
Change-type: patch
Signed-off-by: Ken Bannister <kb2ma@runbox.com>
2025-02-24 20:22:59 -05:00
1352c5c823 v20.2.3 2025-01-15 18:21:09 +00:00
c86eb97010 Merge pull request #2910 from balena-io/clean-eslint
Remove unused old eslint version files
2025-01-15 18:19:22 +00:00
53be743b9d Remove unused old eslint version files
Change-type: patch
2025-01-15 08:37:35 -03:00
d9f21b4c3f v20.2.2 2025-01-12 14:49:07 +00:00
261ab398dd Merge pull request #2906 from balena-io/bump-balena-image-fs
Use the promises namespace of balena-image-fs
2025-01-12 16:48:08 +02:00
f28a9992e4 Deduplicate dependencies 2025-01-09 23:33:54 +02:00
29e7827eb1 Use the promises namespace of balena-image-fs
Change-type: patch
2025-01-09 23:33:54 +02:00
1d77cf3665 Update balena-device-init to 8.1.3 & balena-image-fs to 7.3.0
Update balena-device-init from 8.1.0 to 8.1.3
Update balena-image-fs from 7.0.6 to 7.3.0

Change-type: patch
2025-01-09 23:33:54 +02:00
017c767f61 v20.2.1 2025-01-01 18:05:02 +00:00
7d79c4e24b Merge pull request #2905 from balena-io/update-balena-preload-17.0.0
Update balena-preload to 17.0.0
2025-01-01 20:04:01 +02:00
60bc5092e0 Update balena-preload to 17.0.0
Update balena-preload from 16.0.0 to 17.0.0

Change-type: patch
2024-12-31 20:42:54 +02:00
a33a794931 v20.2.0 2024-12-31 18:41:50 +00:00
f0ede6fca2 Merge pull request #2901 from balena-io/balena-device-init-v-8-1-0
os configure: Locate the boot partition w/o using the device-type.json's partition field
2024-12-31 10:40:54 -08:00
dbe177e766 os configure: Give precedence to the boot partition located in the image over the device-type.json contents
Update balena-device-init from 8.0.0 to 8.1.0

Change-type: minor
2024-12-31 19:48:11 +02:00
09f80730a8 Deduplicate dependencies 2024-12-31 19:47:11 +02:00
327d28c103 v20.1.6 2024-12-30 17:16:14 +00:00
56ab785a82 Merge pull request #2903 from balena-io/os-configure-tests
Add more realistic os configure tests
2024-12-30 17:15:09 +00:00
305d65d5ed Add more realistic os configure tests
Change-type: patch
2024-12-30 18:44:07 +02:00
c4d3686a34 Deduplicate dependencies 2024-12-30 15:57:12 +02:00
ce06854b55 v20.1.5 2024-12-20 17:55:24 +00:00
8db05cc8a7 Merge pull request #2902 from oskarwilliams/express-version-4.21.2
Update shrinkwrapped express to v4.21.2
2024-12-20 17:54:36 +00:00
7a22c987d2 Update shrinkwrapped express to v4.21.2
Change-type: patch
2024-12-20 17:23:28 +00:00
45efbcdfe3 v20.1.4 2024-12-20 16:57:38 +00:00
d6a9b78b3e Merge pull request #2899 from balena-io/balena-device-init-v8
Update balena-device-init to 8.0.0
2024-12-20 16:56:43 +00:00
e8ac3ea960 Update balena-device-init to 8.0.0
Update balena-device-init from 7.0.1 to 8.0.0

Change-type: patch
See: https://github.com/balena-io-modules/balena-device-init/pull/46
2024-12-20 18:36:50 +02:00
0ffa0f85a2 v20.1.3 2024-12-20 14:33:05 +00:00
5e7479f60e Merge pull request #2898 from balena-io/bump-oclif-and-oclif-core
Update oclif to 4.17.0 and @oclif/core 4.1.0
2024-12-20 11:32:01 -03:00
07365c45f2 Update oclif to 4.17.0 and @oclif/core 4.1.0
Change-type: patch
2024-12-20 11:09:29 -03:00
e5076434c6 v20.1.2 2024-12-17 11:35:00 +00:00
5d687f5a55 Merge pull request #2896 from balena-io/unnecessary-promise-resolve-reject
Remove unnecessary `Promise.resolve` and `Promise.reject`
2024-12-17 11:34:05 +00:00
e192767156 Remove unnecessary Promise.resolve and Promise.reject
Change-type: patch
2024-12-16 21:12:02 +00:00
5a8d2fad5f v20.1.1 2024-12-16 17:36:44 +00:00
45f482fad1 Merge pull request #2895 from balena-io/bump-balena-lint-v9
Update @balena/lint to v9.1.3
2024-12-16 17:35:45 +00:00
c0e7ae9c91 Update @balena/lint to v9.1.3
Update @balena/lint from 8.0.0 to 9.1.3

Change-type: patch
2024-12-16 14:09:47 -03:00
93 changed files with 5885 additions and 2213 deletions

View File

@ -1,2 +0,0 @@
/completion/*
/bin/*

View File

@ -1,21 +0,0 @@
module.exports = {
extends: ['./node_modules/@balena/lint/config/.eslintrc.js'],
parserOptions: {
project: 'tsconfig.dev.json',
},
root: true,
rules: {
ignoreDefinitionFiles: 0,
// to avoid the `warning Forbidden non-null assertion @typescript-eslint/no-non-null-assertion`
'@typescript-eslint/no-non-null-assertion': 'off',
'@typescript-eslint/no-shadow': 'off',
'@typescript-eslint/no-var-requires': 'off',
'no-restricted-imports': [
'error',
{
paths: ['resin-cli-visuals', 'chalk', 'common-tags', 'resin-cli-form'],
},
],
'@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
},
};

View File

@ -39,7 +39,7 @@ runs:
run: tar -xf ${{ runner.temp }}/custom.tgz
- name: Setup Node.js
uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4
uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4
with:
node-version: ${{ inputs.NODE_VERSION }}
cache: npm
@ -94,7 +94,7 @@ runs:
runner_arch="$(echo "${RUNNER_ARCH}" | tr '[:upper:]' '[:lower:]')"
if [[ $runner_os =~ darwin|macos|osx ]]; then
CSC_KEY_PASSWORD=${{ fromJSON(inputs.secrets).APPLE_SIGNING_PASSWORD }}
CSC_KEY_PASSWORD='${{ fromJSON(inputs.secrets).APPLE_SIGNING_PASSWORD }}'
CSC_KEYCHAIN=signing_temp
CSC_LINK=${{ fromJSON(inputs.secrets).APPLE_SIGNING }}
@ -135,7 +135,7 @@ runs:
XCODE_APP_LOADER_TEAM_ID: ${{ inputs.XCODE_APP_LOADER_TEAM_ID }}
- name: Upload artifacts
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4
uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4
with:
name: gh-release-${{ github.event.pull_request.head.sha || github.event.head_commit.id }}-${{ strategy.job-index }}
path: dist

View File

@ -26,7 +26,7 @@ runs:
steps:
# https://github.com/actions/setup-node#caching-global-packages-data
- name: Setup Node.js
uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4
uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4
with:
node-version: ${{ inputs.NODE_VERSION }}
cache: npm
@ -58,7 +58,7 @@ runs:
run: tar --exclude-vcs -acf ${{ runner.temp }}/custom.tgz .
- name: Upload custom artifact
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4
uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4
with:
name: custom-${{ github.event.pull_request.head.sha || github.event.head_commit.id }}-${{ runner.os }}-${{ runner.arch }}
path: ${{ runner.temp }}/custom.tgz

View File

@ -1,3 +1,789 @@
- commits:
- subject: Update actions/setup-node digest to cdca736
hash: fe4243809033735a6439f39a1a33dfd00039d656
body: |
Update actions/setup-node
footer:
Change-type: patch
change-type: patch
author: balena-renovate[bot]
nested: []
version: 21.1.6
title: ""
date: 2025-04-03T14:12:06.042Z
- commits:
- subject: Update dockerode/docker-modem dependencies for fixes
hash: b650f8ff6d01d2144886253f93aa1d1867f51980
body: ""
footer:
Change-type: patch
change-type: patch
Signed-off-by: Ken Bannister <kb2ma@runbox.com>
signed-off-by: Ken Bannister <kb2ma@runbox.com>
author: Ken Bannister
nested: []
version: 21.1.5
title: ""
date: 2025-04-03T13:28:08.855Z
- commits:
- subject: Add comment with secure boot signature file example for preload
hash: 28703bb5ae13539ab4c1c597e6a53a5292a7edde
body: ""
footer:
Change-type: patch
change-type: patch
Signed-off-by: Ken Bannister <kb2ma@runbox.com>
signed-off-by: Ken Bannister <kb2ma@runbox.com>
author: Ken Bannister
nested: []
version: 21.1.4
title: ""
date: 2025-04-02T09:16:27.791Z
- commits:
- subject: Fix device detail for open balena
hash: 0d4e411777dd53d83c475da3653ab94176e07d7d
body: ""
footer:
Change-type: patch
change-type: patch
author: Otavio Jacobi
nested: []
version: 21.1.3
title: ""
date: 2025-03-28T16:57:00.250Z
- commits:
- subject: Deny preload for an image with secure boot enabled
hash: 7f2daeebb0973a59682ba4300e1b00bce6f6aead
body: ""
footer:
Change-type: patch
change-type: patch
Signed-off-by: Ken Bannister <kb2ma@runbox.com>
signed-off-by: Ken Bannister <kb2ma@runbox.com>
author: Ken Bannister
nested: []
version: 21.1.2
title: ""
date: 2025-03-27T12:20:22.883Z
- commits:
- subject: Bump balena-sdk to 21.3.0
hash: e82906872538a7401e31bd52e662e8356a89d413
body: |
Update balena-sdk from 21.2.1 to 21.3.0
footer:
Change-type: patch
change-type: patch
author: Otavio Jacobi
nested:
- commits:
- subject: "device: add `changed_api_heartbeat_state_on__date` to typings"
hash: bfa52cf58c7d06859a1b5c6f62ff7c71324d3d20
body: ""
footer:
Change-type: minor
change-type: minor
author: Otavio Jacobi
nested: []
version: balena-sdk-21.3.0
title: ""
date: 2025-03-26T19:54:35.558Z
- commits:
- subject: fix linting
hash: 13320aad81ba3dfc4950b9960f015b058222c4be
body: ""
footer:
Change-type: patch
change-type: patch
author: Otavio Jacobi
nested: []
version: balena-sdk-21.2.2
title: ""
date: 2025-03-26T18:55:53.610Z
version: 21.1.1
title: ""
date: 2025-03-26T20:34:44.546Z
- commits:
- subject: Add support for new requirement labels feature
hash: 42c50ef8aed110b317a0472d928bf75e372b4c0b
body: |
Updates @balena/compose to v7 to include this new feature.
footer:
See: https://balena.fibery.io/Work/Project/Refactoring-container-contracts-1205
see: https://balena.fibery.io/Work/Project/Refactoring-container-contracts-1205
Depends-on: https://github.com/balena-io-modules/balena-compose/pull/64
depends-on: https://github.com/balena-io-modules/balena-compose/pull/64
Change-type: minor
change-type: minor
author: Felipe Lalanne
nested: []
version: 21.1.0
title: ""
date: 2025-03-12T19:34:17.610Z
- commits:
- subject: Drop support for OS versions <2.14.0
hash: aad62d1ccd11ebb69b1035d5b95aef93d384bfd5
body: ""
footer:
Change-type: major
change-type: major
author: myarmolinsky
nested: []
- subject: "api-key generate: Add required argument `expiryDate`"
hash: ecc6f80164fca3c0cde42b140b6d7404abe8c877
body: ""
footer:
Change-type: major
change-type: major
author: myarmolinsky
nested: []
- subject: Update `balena-preload` to 18.0.1
hash: 9d3120b144c2c017eda55463b034f1561d264213
body: ""
footer:
Change-type: patch
change-type: patch
author: myarmolinsky
nested: []
- subject: Add dependency `date-fns`
hash: ed0e03ddb274da294f719dc0e307ec37591e10d7
body: ""
footer:
Change-type: patch
change-type: patch
author: myarmolinsky
nested: []
- subject: Update `balena-sdk` to 21.2.1
hash: 8fe6d6c0268f69bcf3bcac3c57470272b959e9b0
body: ""
footer:
Change-type: patch
change-type: patch
author: myarmolinsky
nested: []
version: 21.0.0
title: ""
date: 2025-03-11T14:42:28.479Z
- commits:
- subject: Update TypeScript to 5.8.2
hash: a10156a441b737275cabfb03bd10bfc5aba7bc88
body: ""
footer:
Change-type: patch
change-type: patch
author: Thodoris Greasidis
nested: []
version: 20.2.10
title: ""
date: 2025-03-10T17:33:09.548Z
- commits:
- subject: Fix CORS issue with X-Balena-Client header
hash: 64d19438042921e89c522f022327ead85b286e9f
body: ""
footer:
Change-type: patch
change-type: patch
See: https://balena.fibery.io/Work/Project/Extend-the-X-Balena-Client-header-to-include-the-UI-CLI-version-as-well-1174
see: https://balena.fibery.io/Work/Project/Extend-the-X-Balena-Client-header-to-include-the-UI-CLI-version-as-well-1174
author: Thodoris Greasidis
nested: []
version: 20.2.9
title: ""
date: 2025-02-26T12:52:06.672Z
- commits:
- subject: Update balena-config-json dependency and fix test
hash: 93039b010db15fbf1c0d17d4ed8f0db554064de4
body: ""
footer:
Change-type: patch
change-type: patch
Signed-off-by: Ken Bannister <kb2ma@runbox.com>
signed-off-by: Ken Bannister <kb2ma@runbox.com>
author: Ken Bannister
nested: []
version: 20.2.8
title: ""
date: 2025-02-26T00:22:14.010Z
- commits:
- subject: Use the CLI version in the X-Balena-Client header
hash: bef5221ed891db12a0b760f12fc9654e2f4e241b
body: ""
footer:
Change-type: patch
change-type: patch
See: https://balena.fibery.io/Work/Project/Extend-the-X-Balena-Client-header-to-include-the-UI-CLI-version-as-well-1174
see: https://balena.fibery.io/Work/Project/Extend-the-X-Balena-Client-header-to-include-the-UI-CLI-version-as-well-1174
author: Thodoris Greasidis
nested: []
version: 20.2.7
title: ""
date: 2025-02-25T20:21:00.603Z
- commits:
- subject: Update actions/upload-artifact digest to 4cec3d8
hash: 6f0f7350cf65c35abd099a901266821c218478eb
body: |
Update actions/upload-artifact
footer:
Change-type: patch
change-type: patch
author: balena-renovate[bot]
nested: []
version: 20.2.6
title: ""
date: 2025-02-25T19:13:48.297Z
- commits:
- subject: Update actions/setup-node digest to 1d0ff46
hash: cddea24cefdfef475731e0a7d2bdec4992959a6b
body: |
Update actions/setup-node
footer:
Change-type: patch
change-type: patch
author: balena-renovate[bot]
nested: []
version: 20.2.5
title: ""
date: 2025-02-25T18:10:47.617Z
- commits:
- subject: Pin docker-modem and dockerode to avoid regression
hash: 2cba82e914c720e75b68bd4370a2a92b4d4a7ba0
body: ""
footer:
Change-type: patch
change-type: patch
Signed-off-by: Ken Bannister <kb2ma@runbox.com>
signed-off-by: Ken Bannister <kb2ma@runbox.com>
author: Ken Bannister
nested: []
version: 20.2.4
title: ""
date: 2025-02-25T17:17:00.607Z
- commits:
- subject: Remove unused old eslint version files
hash: 53be743b9dcecbb2f0d8841b6663e7af1a9006c3
body: ""
footer:
Change-type: patch
change-type: patch
author: Otavio Jacobi
nested: []
version: 20.2.3
title: ""
date: 2025-01-15T18:21:02.810Z
- commits:
- subject: Use the promises namespace of balena-image-fs
hash: 29e7827eb1dbe4b0395df51f09cea38c3b2fb2e4
body: ""
footer:
Change-type: patch
change-type: patch
author: Thodoris Greasidis
nested: []
- subject: Update balena-device-init to 8.1.3 & balena-image-fs to 7.3.0
hash: 1d77cf36652eaca007a25f6ebb85c8509662d576
body: |
Update balena-device-init from 8.1.0 to 8.1.3
Update balena-image-fs from 7.0.6 to 7.3.0
footer:
Change-type: patch
change-type: patch
author: Thodoris Greasidis
nested:
- commits:
- subject: Drop Bluebird from devDependencies
hash: e4b5c71032112664ba8dac4c9edc5dad5445818d
body: ""
footer:
Change-type: patch
change-type: patch
author: Thodoris Greasidis
nested: []
- subject: "flowzone: Remove empty with clause"
hash: e83b4ef55eeebffb86d0bcda78dd45ee0651c76f
body: ""
footer:
Change-type: patch
change-type: patch
author: Thodoris Greasidis
nested: []
- subject: Add the promises namespace as part of the exposed fs
hash: 9b73f44020de213a682afc21092e20caf68eaff8
body: ""
footer:
Change-type: minor
change-type: minor
author: Thodoris Greasidis
nested: []
version: balena-image-fs-7.3.0
title: ""
date: 2025-01-06T14:31:31.244Z
- commits:
- subject: Update dependency @types/node to v20
hash: 4a5f55ae8752b1b28638d3f4016910d5b2057d80
body: |
Update @types/node from 18.11.18 to 20.10.6
footer:
Change-type: patch
change-type: patch
author: Self-hosted Renovate Bot
nested: []
version: balena-image-fs-7.2.2
title: ""
date: 2024-01-02T18:50:24.990Z
- commits:
- subject: Remove repo config from flowzone.yml
hash: 3e0053a097a04c1d0c47139bc53787f07bfeb014
body: |
This functionality is being deprecated in Flowzone.
See: https://github.com/product-os/flowzone/pull/833
footer:
Change-type: patch
change-type: patch
Signed-off-by: Kyle Harding <kyle@balena.io>
signed-off-by: Kyle Harding <kyle@balena.io>
author: Kyle Harding
nested: []
version: balena-image-fs-7.2.1
title: ""
date: 2023-12-19T21:54:42.129Z
- commits:
- subject: Add support for Node 18
hash: 2a3bef60d9688dce26bd1c9546a28c98cf6d2566
body: ""
footer:
Change-type: minor
change-type: minor
author: Akis Kesoglou
nested: []
version: balena-image-fs-7.2.0
title: ""
date: 2023-01-20T14:27:37.169Z
- commits:
- subject: Update dependencies
hash: 9acc4bd2e793076095880f1924f8540b4d95fa9c
body: ""
footer:
Change-type: patch
change-type: patch
author: ab77
nested: []
version: balena-image-fs-7.1.2
title: ""
date: 2023-01-05T22:29:15.022Z
- commits:
- subject: Update dependency jsdoc-to-markdown to 8.0.0
hash: 5cf71efa734819263bd75c9938cb4367f90e1391
body: |
Update jsdoc-to-markdown to 8.0.0
Update jsdoc-to-markdown from 7.1.1 to 8.0.0
footer:
Change-type: patch
change-type: patch
author: Renovate Bot
nested: []
version: balena-image-fs-7.1.1
title: ""
date: 2022-12-20T02:52:28.901Z
- commits:
- subject: update dependencies
hash: 71a5bb92caa0658e45d1fb36e8652ca5ea50dee9
body: ""
footer:
Change-type: minor
change-type: minor
author: Zane Hitchcox
nested: []
version: balena-image-fs-7.1.0
title: ""
date: 2022-12-13T00:03:20.006Z
- commits:
- subject: "README: Remove the travisci badge"
hash: 3602a9c0c503502229d12cc1ec1de5bbcbc1b13c
body: ""
footer:
Change-type: patch
change-type: patch
author: Thodoris Greasidis
nested: []
version: balena-device-init-8.1.3
title: ""
date: 2025-01-09T21:25:00.155Z
- commits:
- subject: Use the promises namespace of balena-image-fs
hash: de0af086c5d9124e4bbe5d66bf9aafe0f6c5f11b
body: |
Update balena-image-fs from 7.0.6 to 7.3.0
footer:
Change-type: patch
change-type: patch
author: Thodoris Greasidis
nested:
- commits:
- subject: Drop Bluebird from devDependencies
hash: e4b5c71032112664ba8dac4c9edc5dad5445818d
body: ""
footer:
Change-type: patch
change-type: patch
author: Thodoris Greasidis
nested: []
- subject: "flowzone: Remove empty with clause"
hash: e83b4ef55eeebffb86d0bcda78dd45ee0651c76f
body: ""
footer:
Change-type: patch
change-type: patch
author: Thodoris Greasidis
nested: []
- subject: Add the promises namespace as part of the exposed fs
hash: 9b73f44020de213a682afc21092e20caf68eaff8
body: ""
footer:
Change-type: minor
change-type: minor
author: Thodoris Greasidis
nested: []
version: balena-image-fs-7.3.0
title: ""
date: 2025-01-06T14:31:31.244Z
- commits:
- subject: Update dependency @types/node to v20
hash: 4a5f55ae8752b1b28638d3f4016910d5b2057d80
body: |
Update @types/node from 18.11.18 to 20.10.6
footer:
Change-type: patch
change-type: patch
author: Self-hosted Renovate Bot
nested: []
version: balena-image-fs-7.2.2
title: ""
date: 2024-01-02T18:50:24.990Z
- commits:
- subject: Remove repo config from flowzone.yml
hash: 3e0053a097a04c1d0c47139bc53787f07bfeb014
body: |
This functionality is being deprecated in Flowzone.
See: https://github.com/product-os/flowzone/pull/833
footer:
Change-type: patch
change-type: patch
Signed-off-by: Kyle Harding <kyle@balena.io>
signed-off-by: Kyle Harding <kyle@balena.io>
author: Kyle Harding
nested: []
version: balena-image-fs-7.2.1
title: ""
date: 2023-12-19T21:54:42.129Z
- commits:
- subject: Add support for Node 18
hash: 2a3bef60d9688dce26bd1c9546a28c98cf6d2566
body: ""
footer:
Change-type: minor
change-type: minor
author: Akis Kesoglou
nested: []
version: balena-image-fs-7.2.0
title: ""
date: 2023-01-20T14:27:37.169Z
- commits:
- subject: Update dependencies
hash: 9acc4bd2e793076095880f1924f8540b4d95fa9c
body: ""
footer:
Change-type: patch
change-type: patch
author: ab77
nested: []
version: balena-image-fs-7.1.2
title: ""
date: 2023-01-05T22:29:15.022Z
- commits:
- subject: Update dependency jsdoc-to-markdown to 8.0.0
hash: 5cf71efa734819263bd75c9938cb4367f90e1391
body: |
Update jsdoc-to-markdown to 8.0.0
Update jsdoc-to-markdown from 7.1.1 to 8.0.0
footer:
Change-type: patch
change-type: patch
author: Renovate Bot
nested: []
version: balena-image-fs-7.1.1
title: ""
date: 2022-12-20T02:52:28.901Z
- commits:
- subject: update dependencies
hash: 71a5bb92caa0658e45d1fb36e8652ca5ea50dee9
body: ""
footer:
Change-type: minor
change-type: minor
author: Zane Hitchcox
nested: []
version: balena-image-fs-7.1.0
title: ""
date: 2022-12-13T00:03:20.006Z
version: balena-device-init-8.1.2
title: ""
date: 2025-01-09T20:28:30.604Z
- commits:
- subject: Convert some parts to async await and simplify
hash: eb1105cd738998bae9c3dd604a8d25f71eb38e19
body: ""
footer:
Change-type: patch
change-type: patch
author: Thodoris Greasidis
nested: []
- subject: Avoid unnecessary destructuring
hash: 4cea6b650ec1c911cf2fc12a4a1dc410c0e7d075
body: ""
footer:
Change-type: patch
change-type: patch
author: Thodoris Greasidis
nested: []
version: balena-device-init-8.1.1
title: ""
date: 2025-01-06T06:37:04.147Z
version: 20.2.2
title: ""
date: 2025-01-12T14:49:03.225Z
- commits:
- subject: Update balena-preload to 17.0.0
hash: 60bc5092e0d6014aecf57d157cac536b3c645f0f
body: |
Update balena-preload from 16.0.0 to 17.0.0
footer:
Change-type: patch
change-type: patch
author: Thodoris Greasidis
nested:
- commits:
- subject: Improve typings
hash: 67f0f3e9e3b4b37fbaeef2843dc4171a7d661c41
body: ""
footer:
Change-type: patch
change-type: patch
author: Thodoris Greasidis
nested: []
- subject: Stop returning Bluebird promises & drop it from the dependencies
hash: 85375cbc1fdb0ce1ad08756f178ed55596e606d9
body: ""
footer:
Change-type: major
change-type: major
author: Thodoris Greasidis
nested: []
version: balena-preload-17.0.0
title: ""
date: 2024-10-21T16:20:33.486Z
version: 20.2.1
title: ""
date: 2025-01-01T18:04:56.723Z
- commits:
- subject: "os configure: Give precedence to the boot partition located in the
image over the device-type.json contents"
hash: dbe177e76639040c5fac49746fbbcee2e3360d0a
body: |
Update balena-device-init from 8.0.0 to 8.1.0
footer:
Change-type: minor
change-type: minor
author: Thodoris Greasidis
nested:
- commits:
- subject: Try to find the boot partition by inspecting the image
hash: de53f11c56133bf2757c5361f8dbc6afa9e826fc
body: ""
footer:
Change-type: minor
change-type: minor
author: Thodoris Greasidis
nested: []
version: balena-device-init-8.1.0
title: ""
date: 2024-12-30T12:41:49.474Z
- commits:
- subject: Drop the unnecessary eslint.config.js
hash: 91d587f34bf985bf65e6c21278e7cd70f9281656
body: ""
footer:
Change-type: patch
change-type: patch
author: Thodoris Greasidis
nested: []
- subject: "packacke.json: Explicitly set type commonjs"
hash: f6a0be1d12f09d7d910e5ae35f48f1268542a74e
body: ""
footer:
Change-type: patch
change-type: patch
author: Thodoris Greasidis
nested: []
version: balena-device-init-8.0.1
title: ""
date: 2024-12-19T13:41:14.170Z
version: 20.2.0
title: ""
date: 2024-12-31T18:41:45.907Z
- commits:
- subject: Add more realistic os configure tests
hash: 305d65d5ed07cce767599ac4e5dd131e2a87d10d
body: ""
footer:
Change-type: patch
change-type: patch
author: Thodoris Greasidis
nested: []
version: 20.1.6
title: ""
date: 2024-12-30T17:16:09.432Z
- commits:
- subject: Update shrinkwrapped express to v4.21.2
hash: 7a22c987d24aca0754bde5618c232424ede72656
body: ""
footer:
Change-type: patch
change-type: patch
author: Oskar Williams
nested: []
version: 20.1.5
title: ""
date: 2024-12-20T17:55:20.670Z
- commits:
- subject: Update balena-device-init to 8.0.0
hash: e8ac3ea960b1765470b496dbd714036d9b2ecd93
body: |
Update balena-device-init from 7.0.1 to 8.0.0
footer:
Change-type: patch
change-type: patch
See: https://github.com/balena-io-modules/balena-device-init/pull/46
see: https://github.com/balena-io-modules/balena-device-init/pull/46
author: Thodoris Greasidis
nested:
- commits:
- subject: Avoid running linting in the custom tests
hash: 420f93d5b2c60b67157897d8bbe0fab315c98b54
body: |
Balena-lint for some reason was failing in the
windows custom test runner.
footer:
Change-type: patch
change-type: patch
author: Thodoris Greasidis
nested: []
- subject: Stop returning Bluebird promises
hash: b76200a113fee69e11bf182945c382081f8d25e1
body: ""
footer:
Change-type: major
change-type: major
author: Thodoris Greasidis
nested: []
- subject: "package: Publish only the build & typings folders"
hash: d1754650272913577b3ce0786941ad54d4acddc8
body: ""
footer:
Change-type: major
change-type: major
author: Thodoris Greasidis
nested: []
- subject: Convert to TypeScript with es6 module notation
hash: 3576d2c423b294a8703f5474f72a9b906cef927a
body: ""
footer:
Change-type: major
change-type: major
author: Thodoris Greasidis
nested: []
- subject: Replace gulp, coffeelint & mochainon with tsc, @balena/lint, mocha,
chai & sinon
hash: 4c0cf8af370962e8eefda3a2855113629c4ffbb5
body: ""
footer:
Change-type: patch
change-type: patch
author: Thodoris Greasidis
nested: []
- subject: Drop support for node <20.6.0
hash: b7c76be409a7a691db66466bc6d6eb746c0ca51e
body: ""
footer:
Change-type: major
change-type: major
author: Thodoris Greasidis
nested: []
version: balena-device-init-8.0.0
title: ""
date: 2024-12-18T17:01:33.082Z
- commits:
- subject: "flowzone: Update runner versions"
hash: ba1222ac43ca13b720b64d18129193be8b99af5b
body: ""
footer:
Change-type: patch
change-type: patch
author: Thodoris Greasidis
nested: []
- subject: Pin etcher-sdk to 9.0.8 to match resin-device-operations and fix tests
hash: 65cde1b940b7bdcc64d34534afa4d9b7b1d14303
body: ""
footer:
Change-type: patch
change-type: patch
author: Thodoris Greasidis
nested: []
version: balena-device-init-7.0.2
title: ""
date: 2024-12-17T20:16:06.532Z
version: 20.1.4
title: ""
date: 2024-12-20T16:57:31.612Z
- commits:
- subject: Update oclif to 4.17.0 and @oclif/core 4.1.0
hash: 07365c45f2d9a46094faa60d62395c4ba139c9f6
body: ""
footer:
Change-type: patch
change-type: patch
author: Otavio Jacobi
nested: []
version: 20.1.3
title: ""
date: 2024-12-20T14:32:59.953Z
- commits:
- subject: Remove unnecessary `Promise.resolve` and `Promise.reject`
hash: e192767156ef7123be76fb838b8cf1a4d80aeb54
body: ""
footer:
Change-type: patch
change-type: patch
author: Pagan Gazzard
nested: []
version: 20.1.2
title: ""
date: 2024-12-17T11:34:55.264Z
- commits:
- subject: Update @balena/lint to v9.1.3
hash: c0e7ae9c915d147faab215226d2fd04c35c72176
body: |
Update @balena/lint from 8.0.0 to 9.1.3
footer:
Change-type: patch
change-type: patch
author: Otavio Jacobi
nested: []
version: 20.1.1
title: ""
date: 2024-12-16T17:36:39.581Z
- commits:
- subject: "`device os-update`: Add handling for updates that require takeover"
hash: 703dbd01c952474f28cbe3105a5ebaa41b4a0566

View File

@ -4,6 +4,251 @@ 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/).
## 21.1.6 - 2025-04-03
* Update actions/setup-node digest to cdca736 [balena-renovate[bot]]
## 21.1.5 - 2025-04-03
* Update dockerode/docker-modem dependencies for fixes [Ken Bannister]
## 21.1.4 - 2025-04-02
* Add comment with secure boot signature file example for preload [Ken Bannister]
## 21.1.3 - 2025-03-28
* Fix device detail for open balena [Otavio Jacobi]
## 21.1.2 - 2025-03-27
* Deny preload for an image with secure boot enabled [Ken Bannister]
## 21.1.1 - 2025-03-26
<details>
<summary> Bump balena-sdk to 21.3.0 [Otavio Jacobi] </summary>
> ### balena-sdk-21.3.0 - 2025-03-26
>
> * device: add `changed_api_heartbeat_state_on__date` to typings [Otavio Jacobi]
>
> ### balena-sdk-21.2.2 - 2025-03-26
>
> * fix linting [Otavio Jacobi]
>
</details>
## 21.1.0 - 2025-03-12
* Add support for new requirement labels feature [Felipe Lalanne]
## 21.0.0 - 2025-03-11
* Drop support for OS versions <2.14.0 [myarmolinsky]
* api-key generate: Add required argument `expiryDate` [myarmolinsky]
* Update `balena-preload` to 18.0.1 [myarmolinsky]
* Add dependency `date-fns` [myarmolinsky]
* Update `balena-sdk` to 21.2.1 [myarmolinsky]
## 20.2.10 - 2025-03-10
* Update TypeScript to 5.8.2 [Thodoris Greasidis]
## 20.2.9 - 2025-02-26
* Fix CORS issue with X-Balena-Client header [Thodoris Greasidis]
## 20.2.8 - 2025-02-26
* Update balena-config-json dependency and fix test [Ken Bannister]
## 20.2.7 - 2025-02-25
* Use the CLI version in the X-Balena-Client header [Thodoris Greasidis]
## 20.2.6 - 2025-02-25
* Update actions/upload-artifact digest to 4cec3d8 [balena-renovate[bot]]
## 20.2.5 - 2025-02-25
* Update actions/setup-node digest to 1d0ff46 [balena-renovate[bot]]
## 20.2.4 - 2025-02-25
* Pin docker-modem and dockerode to avoid regression [Ken Bannister]
## 20.2.3 - 2025-01-15
* Remove unused old eslint version files [Otavio Jacobi]
## 20.2.2 - 2025-01-12
* Use the promises namespace of balena-image-fs [Thodoris Greasidis]
<details>
<summary> Update balena-device-init to 8.1.3 & balena-image-fs to 7.3.0 [Thodoris Greasidis] </summary>
> ### balena-image-fs-7.3.0 - 2025-01-06
>
> * Drop Bluebird from devDependencies [Thodoris Greasidis]
> * flowzone: Remove empty with clause [Thodoris Greasidis]
> * Add the promises namespace as part of the exposed fs [Thodoris Greasidis]
>
> ### balena-image-fs-7.2.2 - 2024-01-02
>
> * Update dependency @types/node to v20 [Self-hosted Renovate Bot]
>
> ### balena-image-fs-7.2.1 - 2023-12-19
>
> * Remove repo config from flowzone.yml [Kyle Harding]
>
> ### balena-image-fs-7.2.0 - 2023-01-20
>
> * Add support for Node 18 [Akis Kesoglou]
>
> ### balena-image-fs-7.1.2 - 2023-01-05
>
> * Update dependencies [ab77]
>
> ### balena-image-fs-7.1.1 - 2022-12-20
>
> * Update dependency jsdoc-to-markdown to 8.0.0 [Renovate Bot]
>
> ### balena-image-fs-7.1.0 - 2022-12-13
>
> * update dependencies [Zane Hitchcox]
>
> ### balena-device-init-8.1.3 - 2025-01-09
>
> * README: Remove the travisci badge [Thodoris Greasidis]
>
> ### balena-device-init-8.1.2 - 2025-01-09
>
>
> <details>
> <summary> Use the promises namespace of balena-image-fs [Thodoris Greasidis] </summary>
>
>> #### balena-image-fs-7.3.0 - 2025-01-06
>>
>> * Drop Bluebird from devDependencies [Thodoris Greasidis]
>> * flowzone: Remove empty with clause [Thodoris Greasidis]
>> * Add the promises namespace as part of the exposed fs [Thodoris Greasidis]
>>
>> #### balena-image-fs-7.2.2 - 2024-01-02
>>
>> * Update dependency @types/node to v20 [Self-hosted Renovate Bot]
>>
>> #### balena-image-fs-7.2.1 - 2023-12-19
>>
>> * Remove repo config from flowzone.yml [Kyle Harding]
>>
>> #### balena-image-fs-7.2.0 - 2023-01-20
>>
>> * Add support for Node 18 [Akis Kesoglou]
>>
>> #### balena-image-fs-7.1.2 - 2023-01-05
>>
>> * Update dependencies [ab77]
>>
>> #### balena-image-fs-7.1.1 - 2022-12-20
>>
>> * Update dependency jsdoc-to-markdown to 8.0.0 [Renovate Bot]
>>
>> #### balena-image-fs-7.1.0 - 2022-12-13
>>
>> * update dependencies [Zane Hitchcox]
>>
>
> </details>
>
>
> ### balena-device-init-8.1.1 - 2025-01-06
>
> * Convert some parts to async await and simplify [Thodoris Greasidis]
> * Avoid unnecessary destructuring [Thodoris Greasidis]
>
</details>
## 20.2.1 - 2025-01-01
<details>
<summary> Update balena-preload to 17.0.0 [Thodoris Greasidis] </summary>
> ### balena-preload-17.0.0 - 2024-10-21
>
> * Improve typings [Thodoris Greasidis]
> * Stop returning Bluebird promises & drop it from the dependencies [Thodoris Greasidis]
>
</details>
## 20.2.0 - 2024-12-31
<details>
<summary> os configure: Give precedence to the boot partition located in the image over the device-type.json contents [Thodoris Greasidis] </summary>
> ### balena-device-init-8.1.0 - Invalid date
>
> * Try to find the boot partition by inspecting the image [Thodoris Greasidis]
>
> ### balena-device-init-8.0.1 - 2024-12-19
>
> * Drop the unnecessary eslint.config.js [Thodoris Greasidis]
> * packacke.json: Explicitly set type commonjs [Thodoris Greasidis]
>
</details>
## 20.1.6 - 2024-12-30
* Add more realistic os configure tests [Thodoris Greasidis]
## 20.1.5 - 2024-12-20
* Update shrinkwrapped express to v4.21.2 [Oskar Williams]
## 20.1.4 - 2024-12-20
<details>
<summary> Update balena-device-init to 8.0.0 [Thodoris Greasidis] </summary>
> ### balena-device-init-8.0.0 - 2024-12-18
>
> * Avoid running linting in the custom tests [Thodoris Greasidis]
> * Stop returning Bluebird promises [Thodoris Greasidis]
> * package: Publish only the build & typings folders [Thodoris Greasidis]
> * Convert to TypeScript with es6 module notation [Thodoris Greasidis]
> * Replace gulp, coffeelint & mochainon with tsc, @balena/lint, mocha, chai & sinon [Thodoris Greasidis]
> * Drop support for node <20.6.0 [Thodoris Greasidis]
>
> ### balena-device-init-7.0.2 - 2024-12-17
>
> * flowzone: Update runner versions [Thodoris Greasidis]
> * Pin etcher-sdk to 9.0.8 to match resin-device-operations and fix tests [Thodoris Greasidis]
>
</details>
## 20.1.3 - 2024-12-20
* Update oclif to 4.17.0 and @oclif/core 4.1.0 [Otavio Jacobi]
## 20.1.2 - 2024-12-17
* Remove unnecessary `Promise.resolve` and `Promise.reject` [Pagan Gazzard]
## 20.1.1 - 2024-12-16
* Update @balena/lint to v9.1.3 [Otavio Jacobi]
## 20.1.0 - 2024-12-12
* `device os-update`: Add handling for updates that require takeover [myarmolinsky]

View File

@ -305,7 +305,7 @@ async function zipPkg() {
);
}
await fs.mkdirp(path.dirname(outputFile));
await new Promise((resolve, reject) => {
await new Promise<void>((resolve, reject) => {
console.log(`Zipping standalone package to "${outputFile}"...`);
const archive = archiver('zip', {

View File

@ -145,7 +145,7 @@ export async function getCapitanoDoc(): Promise<typeof capitanoDoc> {
throw new Error(`Error parsing section title`);
}
// match[1] has the title, match[2] has the rest
return match && match[2];
return match?.[2];
}),
mdParser.getSectionOfTitle('Installation'),
mdParser.getSectionOfTitle('Choosing a shell (command prompt/terminal)'),

View File

@ -3,7 +3,7 @@ import * as semver from 'semver';
const changeTypes = ['major', 'minor', 'patch'] as const;
const validateChangeType = (maybeChangeType: string = 'minor') => {
const validateChangeType = (maybeChangeType = 'minor') => {
maybeChangeType = maybeChangeType.toLowerCase();
switch (maybeChangeType) {
case 'patch':
@ -136,5 +136,4 @@ async function main() {
}
}
// eslint-disable-next-line @typescript-eslint/no-floating-promises
main();
void main();

View File

@ -19,6 +19,7 @@ import { spawn } from 'child_process';
import * as path from 'path';
import * as fs from 'fs';
import { diffTrimmedLines } from 'diff';
import * as whichMod from 'which';
export const ROOT = path.join(__dirname, '..');
@ -101,7 +102,6 @@ export function loadPackageJson() {
* @returns The program's full path, e.g. 'C:\WINDOWS\System32\OpenSSH\ssh.EXE'
*/
export async function which(program: string): Promise<string> {
const whichMod = await import('which');
let programPath: string;
try {
programPath = await whichMod(program);
@ -132,7 +132,7 @@ export async function whichSpawn(
.on('error', reject)
.on('close', resolve);
} catch (err) {
reject(err);
reject(err as Error);
}
});
} catch (err) {

View File

@ -57,7 +57,10 @@ require('ts-node').register({
project: path.join(rootDir, 'tsconfig.json'),
transpileOnly: true,
});
require('../src/app').run(undefined, { dir: __dirname, development: true });
void require('../src/app').run(undefined, {
dir: __dirname,
development: true,
});
// Modify package.json oclif paths from build/ -> src/, or vice versa
function modifyOclifPaths(revert) {

View File

@ -5,7 +5,7 @@
process.env.UV_THREADPOOL_SIZE = '64';
// Disable oclif registering ts-node
process.env.OCLIF_TS_NODE = 0;
process.env.OCLIF_TS_NODE = '0';
async function run() {
// Use fast-boot to cache require lookups, speeding up startup
@ -18,4 +18,4 @@ async function run() {
await require('../build/app').run(undefined, { dir: __dirname });
}
run();
void run();

View File

@ -326,6 +326,8 @@ or to authenticate requests to the API with an 'Authorization: Bearer <key>' hea
Examples:
$ balena api-key generate "Jenkins Key"
$ balena api-key generate "Jenkins Key" 2025-10-30
$ balena api-key generate "Jenkins Key" never
### Arguments
@ -333,6 +335,10 @@ Examples:
the API key name
#### EXPIRYDATE
the expiry date of the API key as an ISO date string, or "never" for no expiry
## api-key list
### Aliases

32
eslint.config.js Normal file
View File

@ -0,0 +1,32 @@
const { FlatCompat } = require('@eslint/eslintrc');
const compat = new FlatCompat({
baseDirectory: __dirname,
});
module.exports = [
...require('@balena/lint/config/eslint.config'),
...compat.config({
parserOptions: {
project: 'tsconfig.dev.json',
},
ignorePatterns: ['**/generate-completion.js', '**/bin/**/*'],
rules: {
ignoreDefinitionFiles: 0,
'@typescript-eslint/no-non-null-assertion': 'off',
'@typescript-eslint/no-shadow': 'off',
'@typescript-eslint/no-var-requires': 'off',
'@typescript-eslint/no-require-imports': 'off',
'@typescript-eslint/no-unnecessary-type-assertion': 'off',
'@typescript-eslint/prefer-nullish-coalescing': 'warn',
'no-restricted-imports': ['error', {
paths: ['resin-cli-visuals', 'chalk', 'common-tags', 'resin-cli-form'],
}],
'@typescript-eslint/no-unused-vars': ['error', {
argsIgnorePattern: '^_',
caughtErrorsIgnorePattern: '^_',
}],
},
}),
];

5520
npm-shrinkwrap.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
{
"name": "balena-cli",
"version": "20.1.0",
"version": "21.1.6",
"description": "The official balena Command Line Interface",
"main": "./build/app.js",
"homepage": "https://github.com/balena-io/balena-cli",
@ -58,6 +58,7 @@
"build:completion": "node completion/generate-completion.js",
"build:standalone": "ts-node --transpile-only automation/run.ts build:standalone",
"build:installer": "ts-node --transpile-only automation/run.ts build:installer",
"deduplicate-dependencies": "npm dd && git add npm-shrinkwrap.json && git commit --message \"Deduplicate dependencies\"",
"package": "npm run build:fast && npm run build:standalone && npm run build:installer",
"pretest": "npm run build",
"test": "npm run test:shrinkwrap && npm run test:core",
@ -111,7 +112,7 @@
}
},
"devDependencies": {
"@balena/lint": "^8.0.0",
"@balena/lint": "^9.1.3",
"@electron/notarize": "^2.0.0",
"@types/archiver": "^6.0.2",
"@types/bluebird": "^3.5.36",
@ -180,27 +181,27 @@
"mock-fs": "^5.2.0",
"mock-require": "^3.0.3",
"nock": "^13.2.1",
"oclif": "^4.14.0",
"oclif": "^4.17.0",
"rewire": "^7.0.0",
"simple-git": "^3.14.1",
"sinon": "^19.0.0",
"string-to-stream": "^3.0.1",
"ts-node": "^10.4.0",
"typescript": "^5.7.2"
"typescript": "^5.8.2"
},
"dependencies": {
"@balena/compose": "^6.0.0",
"@balena/compose": "^7.0.1",
"@balena/dockerignore": "^1.0.2",
"@balena/env-parsing": "^1.1.8",
"@balena/es-version": "^1.0.1",
"@oclif/core": "^4.0.31",
"@oclif/core": "^4.1.0",
"@sentry/node": "^6.16.1",
"balena-config-json": "^4.2.0",
"balena-device-init": "^7.0.1",
"balena-config-json": "^4.2.2",
"balena-device-init": "^8.1.3",
"balena-errors": "^4.7.3",
"balena-image-fs": "^7.0.6",
"balena-preload": "^16.0.0",
"balena-sdk": "^20.8.0",
"balena-image-fs": "^7.5.0",
"balena-preload": "^18.0.1",
"balena-sdk": "^21.3.0",
"balena-semver": "^2.3.0",
"balena-settings-client": "^5.0.2",
"balena-settings-storage": "^8.1.0",
@ -211,10 +212,11 @@
"cli-truncate": "^2.1.0",
"color-hash": "^1.1.1",
"common-tags": "^1.7.2",
"date-fns": "^4.1.0",
"denymount": "^2.3.0",
"docker-modem": "^5.0.3",
"docker-modem": "^5.0.6",
"docker-progress": "^5.1.3",
"dockerode": "^4.0.2",
"dockerode": "^4.0.5",
"ejs": "^3.1.6",
"etcher-sdk": "9.1.0",
"express": "^4.17.2",
@ -274,6 +276,6 @@
}
},
"versionist": {
"publishedAt": "2024-12-12T14:17:16.574Z"
"publishedAt": "2025-04-03T14:12:06.976Z"
}
}

View File

@ -1,8 +1,8 @@
diff --git a/node_modules/oclif/lib/commands/pack/win.js b/node_modules/oclif/lib/commands/pack/win.js
index ef7f90e..8264b7c 100644
index bfe9205..482519e 100644
--- a/node_modules/oclif/lib/commands/pack/win.js
+++ b/node_modules/oclif/lib/commands/pack/win.js
@@ -76,6 +76,12 @@ InstallDir "\$PROGRAMFILES${arch === 'x64' ? '64' : ''}\\${config.dirname}"
@@ -86,6 +86,12 @@ InstallDir "\$PROGRAMFILES${arch === 'x64' ? '64' : ''}\\${config.dirname}"
${customization}
Section "${config.name} CLI \${VERSION}"
@ -16,20 +16,18 @@ index ef7f90e..8264b7c 100644
File /r bin
File /r client
diff --git a/node_modules/oclif/lib/tarballs/build.js b/node_modules/oclif/lib/tarballs/build.js
index 14d5a6e..7b42a6f 100644
index f0c8d95..a72400e 100644
--- a/node_modules/oclif/lib/tarballs/build.js
+++ b/node_modules/oclif/lib/tarballs/build.js
@@ -200,6 +200,13 @@ const extractCLI = async (tarball, c) => {
@@ -218,6 +218,11 @@ const extractCLI = async (tarball, c) => {
(0, promises_1.rm)(path.join(workspace, path.basename(tarball)), { recursive: true }),
(0, fs_extra_1.remove)(path.join(workspace, 'bin', 'run.cmd')),
]);
+
+ // The oclif installers are a production installation, while the source
+ // `bin` folder may contain a `.fast-boot.json` file of a dev installation.
+ // This has previously led to issues preventing the CLI from starting, so
+ // delete `.fast-boot.json` (if any) from the destination folder.
+ await (0, fs_extra_1.remove)(path.join(workspace, 'bin', '.fast-boot.json'));
+
};
const buildTarget = async (target, c, options) => {
const workspace = c.workspace(target);
if (target.platform === 'win32' && target.arch === 'arm64' && (0, semver_1.lt)(c.nodeVersion, '20.0.0')) {

View File

@ -6,6 +6,10 @@ upstream:
url: 'https://github.com/balena-io/balena-sdk'
- repo: 'balena-config-json'
url: 'https://github.com/balena-io-modules/balena-config-json'
- repo: 'balena-image-fs'
url: 'https://github.com/balena-io-modules/balena-image-fs'
- repo: 'balena-device-init'
url: 'https://github.com/balena-io-modules/balena-device-init'
- repo: 'balena-image-manager'
url: 'https://github.com/balena-io-modules/balena-image-manager'
- repo: 'balena-preload'

View File

@ -101,11 +101,9 @@ async function init() {
/** Execute the oclif parser and the CLI command. */
async function oclifRun(command: string[], options: AppOptions) {
let deprecationPromise: Promise<void>;
let deprecationPromise: Promise<void> | undefined;
// check and enforce the CLI's deprecation policy
if (unsupportedFlag || process.env.BALENARC_UNSUPPORTED) {
deprecationPromise = Promise.resolve();
} else {
if (!(unsupportedFlag || process.env.BALENARC_UNSUPPORTED)) {
const { DeprecationChecker } = await import('./deprecation');
const deprecationChecker = new DeprecationChecker(packageJSON.version);
// warnAndAbortIfDeprecated uses previously cached data only

View File

@ -17,7 +17,16 @@
import { Args, Command } from '@oclif/core';
import { ExpectedError } from '../../errors';
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
import { getBalenaSdk, getCliForm, stripIndent } from '../../utils/lazy';
import {
formatDuration,
intervalToDuration,
isValid,
parseISO,
} from 'date-fns';
// In days
const durations = [1, 7, 30, 90];
async function isLoggedInWithJwt() {
const balena = getBalenaSdk();
@ -41,13 +50,21 @@ export default class GenerateCmd extends Command {
This key can be used to log into the CLI using 'balena login --token <key>',
or to authenticate requests to the API with an 'Authorization: Bearer <key>' header.
`;
public static examples = ['$ balena api-key generate "Jenkins Key"'];
public static examples = [
'$ balena api-key generate "Jenkins Key"',
'$ balena api-key generate "Jenkins Key" 2025-10-30',
'$ balena api-key generate "Jenkins Key" never',
];
public static args = {
name: Args.string({
description: 'the API key name',
required: true,
}),
expiryDate: Args.string({
description:
'the expiry date of the API key as an ISO date string, or "never" for no expiry',
}),
};
public static authenticated = true;
@ -55,9 +72,61 @@ export default class GenerateCmd extends Command {
public async run() {
const { args: params } = await this.parse(GenerateCmd);
let expiryDateResponse: string | number | undefined = params.expiryDate;
let key;
try {
key = await getBalenaSdk().models.apiKey.create(params.name);
if (!expiryDateResponse) {
expiryDateResponse = await getCliForm().ask({
message: 'Please pick an expiry date for the API key',
type: 'list',
choices: [...durations, 'custom', 'never'].map((duration) => ({
name:
duration === 'never'
? 'No expiration'
: typeof duration === 'number'
? formatDuration(
intervalToDuration({
start: 0,
end: duration * 24 * 60 * 60 * 1000,
}),
)
: 'Custom expiration',
value: duration,
})),
});
}
let expiryDate: Date | null;
if (expiryDateResponse === 'never') {
expiryDate = null;
} else if (expiryDateResponse === 'custom') {
do {
expiryDate = parseISO(
await getCliForm().ask({
message:
'Please enter an expiry date for the API key as an ISO date string',
type: 'input',
}),
);
if (!isValid(expiryDate)) {
console.error('Invalid date format');
}
} while (!isValid(expiryDate));
} else if (typeof expiryDateResponse === 'string') {
expiryDate = parseISO(expiryDateResponse);
if (!isValid(expiryDate)) {
throw new Error(
'Invalid date format, please use a valid ISO date string',
);
}
} else {
expiryDate = new Date(
Date.now() + expiryDateResponse * 24 * 60 * 60 * 1000,
);
}
key = await getBalenaSdk().models.apiKey.create({
name: params.name,
expiryDate: expiryDate === null ? null : expiryDate.toISOString(),
});
} catch (e) {
if (e.name === 'BalenaNotLoggedIn') {
if (await isLoggedInWithJwt()) {

View File

@ -50,9 +50,9 @@ export default class RevokeCmd extends Command {
return;
}
await Promise.all(
apiKeyIds.map(
async (id) => await getBalenaSdk().models.apiKey.revoke(Number(id)),
),
apiKeyIds.map(async (id) => {
await getBalenaSdk().models.apiKey.revoke(Number(id));
}),
);
console.log('Successfully revoked the given API keys');
}

View File

@ -368,6 +368,7 @@ ${dockerignoreHelp}
!opts.shouldUploadLogs,
composeOpts.projectPath,
opts.createAsDraft,
project.descriptors,
);
}

View File

@ -91,7 +91,7 @@ export default class DeviceDetectCmd extends Command {
try {
await docker.ping();
return true;
} catch (err) {
} catch {
return false;
}
}),

View File

@ -77,45 +77,59 @@ export default class DeviceCmd extends Command {
const balena = getBalenaSdk();
const device = (await balena.models.device.get(
params.uuid,
options.json
? {
$expand: {
device_tag: {
$select: ['tag_key', 'value'],
},
...expandForAppName.$expand,
let device: ExtendedDevice;
if (options.json) {
const [deviceBase, deviceComputed] = await Promise.all([
balena.models.device.get(params.uuid, {
$expand: {
device_tag: {
$select: ['tag_key', 'value'],
},
}
: {
$select: [
'device_name',
'id',
'overall_status',
'is_online',
'ip_address',
'mac_address',
'last_connectivity_event',
'uuid',
'supervisor_version',
'is_web_accessible',
'note',
'os_version',
'memory_usage',
'memory_total',
'public_address',
'storage_block_device',
'storage_usage',
'storage_total',
'cpu_usage',
'cpu_temp',
'cpu_id',
'is_undervolted',
],
...expandForAppName,
...expandForAppName.$expand,
},
)) as ExtendedDevice;
}),
balena.models.device.get(params.uuid, {
$select: [
'overall_status',
'overall_progress',
'should_be_running__release',
],
}),
]);
device = {
...deviceBase,
...deviceComputed,
} as ExtendedDevice;
} else {
device = (await balena.models.device.get(params.uuid, {
$select: [
'device_name',
'id',
'overall_status',
'is_online',
'ip_address',
'mac_address',
'last_connectivity_event',
'uuid',
'supervisor_version',
'is_web_accessible',
'note',
'os_version',
'memory_usage',
'memory_total',
'public_address',
'storage_block_device',
'storage_usage',
'storage_total',
'cpu_usage',
'cpu_temp',
'cpu_id',
'is_undervolted',
],
...expandForAppName,
})) as ExtendedDevice;
}
if (options.view) {
const open = await import('open');

View File

@ -155,7 +155,7 @@ export default class DeviceInitCmd extends Command {
try {
logger.logDebug(`Process failed, removing device ${device.uuid}`);
await balena.models.device.remove(device.uuid);
} catch (e) {
} catch {
// Ignore removal failures, and throw original error
}
throw e;

View File

@ -135,7 +135,7 @@ export default class DeviceLogsCmd extends Command {
logger.logDebug('Checking we can access device');
try {
await deviceApi.ping();
} catch (e) {
} catch {
const { ExpectedError } = await import('../../errors');
throw new ExpectedError(
`Cannot access device at address ${params.device}. Device may not be in local mode.`,

View File

@ -76,6 +76,6 @@ export default class DeviceRegisterCmd extends Command {
options.deviceType,
);
return result && result.uuid;
return result.uuid;
}
}

View File

@ -82,7 +82,7 @@ export default class DeviceSSHCmd extends Command {
SSH server port number (default 22222) if the target is an IP address or .local
hostname. Otherwise, port number for the balenaCloud gateway (default 22).`,
char: 'p',
parse: async (p) => parseAsInteger(p, 'port'),
parse: (p) => parseAsInteger(p, 'port'),
}),
tty: Flags.boolean({
default: false,
@ -110,13 +110,14 @@ export default class DeviceSSHCmd extends Command {
// Local connection
if (validateLocalHostnameOrIp(params.fleetOrDevice)) {
const { performLocalDeviceSSH } = await import('../../utils/device/ssh');
return await performLocalDeviceSSH({
await performLocalDeviceSSH({
hostname: params.fleetOrDevice,
port: options.port || 'local',
forceTTY: options.tty,
verbose: options.verbose,
service: params.service,
});
return;
}
// Remote connection
@ -132,7 +133,7 @@ export default class DeviceSSHCmd extends Command {
const useProxy = !!proxyConfig && !options.noproxy;
// this will be a tunnelled SSH connection...
await checkNotUsingOfflineMode();
checkNotUsingOfflineMode();
await checkLoggedIn();
const deviceUuid = await getOnlineTargetDeviceUuid(
sdk,

View File

@ -41,7 +41,7 @@ export default class EnvRenameCmd extends Command {
id: Args.integer({
required: true,
description: "variable's numeric database ID",
parse: async (input) => parseAsInteger(input, 'id'),
parse: (input) => parseAsInteger(input, 'id'),
}),
value: Args.string({
required: true,

View File

@ -46,7 +46,7 @@ export default class EnvRmCmd extends Command {
id: Args.integer({
required: true,
description: "variable's numeric database ID",
parse: async (input) => parseAsInteger(input, 'id'),
parse: (input) => parseAsInteger(input, 'id'),
}),
};

View File

@ -16,7 +16,6 @@
*/
import { Args, Command } from '@oclif/core';
import { promisify } from 'util';
import { stripIndent } from '../../utils/lazy';
export default class LocalConfigureCmd extends Command {
@ -237,7 +236,7 @@ export default class LocalConfigureCmd extends Command {
const bootPartition = await getBootPartition(target);
const files = await imagefs.interact(target, bootPartition, async (_fs) => {
return await promisify(_fs.readdir)(this.CONNECTIONS_FOLDER);
return await _fs.promises.readdir(this.CONNECTIONS_FOLDER);
});
let connectionFileName;
@ -246,13 +245,11 @@ export default class LocalConfigureCmd extends Command {
} else if (_.includes(files, 'resin-sample.ignore')) {
// Fresh image, new mode, accoding to https://github.com/balena-os/meta-balena/pull/770/files
await imagefs.interact(target, bootPartition, async (_fs) => {
const readFileAsync = promisify(_fs.readFile);
const writeFileAsync = promisify(_fs.writeFile);
const contents = await readFileAsync(
const contents = await _fs.promises.readFile(
`${this.CONNECTIONS_FOLDER}/resin-sample.ignore`,
{ encoding: 'utf8' },
);
return await writeFileAsync(
await _fs.promises.writeFile(
`${this.CONNECTIONS_FOLDER}/resin-wifi`,
contents,
);
@ -269,13 +266,13 @@ export default class LocalConfigureCmd extends Command {
} else {
// In case there's no file at all (shouldn't happen normally, but the file might have been removed)
await imagefs.interact(target, bootPartition, async (_fs) => {
return await promisify(_fs.writeFile)(
await _fs.promises.writeFile(
`${this.CONNECTIONS_FOLDER}/resin-wifi`,
this.CONNECTION_FILE,
);
});
}
return await this.getConfigurationSchema(bootPartition, connectionFileName);
return this.getConfigurationSchema(bootPartition, connectionFileName);
}
async removeHostname(schema: any) {

View File

@ -132,7 +132,7 @@ export default class LoginCmd extends Command {
// We can safely assume this won't be undefined as doLogin will throw if this call fails
// We also don't need to worry too much about the amount of calls to whoami
// as these are cached by the SDK
const whoamiResult = (await balena.auth.whoami()) as WhoamiResult;
const whoamiResult = (await balena.auth.whoami())!;
if (whoamiResult.actorType !== 'user' && !options.hideExperimentalWarning) {
console.info(stripIndent`
@ -168,7 +168,7 @@ ${messages.reachingOut}`);
async doLogin(
loginOptions: FlagsDef,
balenaUrl: string = 'balena-cloud.com',
balenaUrl = 'balena-cloud.com',
token?: string,
): Promise<void> {
// Token

View File

@ -18,7 +18,6 @@
import { Flags, Args, Command } from '@oclif/core';
import type { Interfaces } from '@oclif/core';
import type * as BalenaSdk from 'balena-sdk';
import { promisify } from 'util';
import * as _ from 'lodash';
import { ExpectedError } from '../../errors';
import * as cf from '../../utils/common-flags';
@ -292,7 +291,7 @@ export default class OsConfigureCmd extends Command {
for (const { name, content } of files) {
await imagefs.interact(image, bootPartition, async (_fs) => {
return await promisify(_fs.writeFile)(
await _fs.promises.writeFile(
path.join(CONNECTIONS_FOLDER, name),
content,
);

View File

@ -37,6 +37,7 @@ import type {
Release,
} from 'balena-sdk';
import type { Preloader } from 'balena-preload';
import type * as Fs from 'fs';
export default class PreloadCmd extends Command {
public static description = stripIndent`
@ -109,7 +110,7 @@ https://github.com/balena-io-examples/staged-releases\
'additional-space': Flags.integer({
description:
'expand the image by this amount of bytes instead of automatically estimating the required amount',
parse: async (x) => parseAsInteger(x, 'additional-space'),
parse: (x) => parseAsInteger(x, 'additional-space'),
}),
'add-certificate': Flags.string({
description: `\
@ -126,7 +127,7 @@ Can be repeated to add multiple certificates.\
dockerPort: Flags.integer({
description:
'Docker daemon TCP port number (hint: 2375 for balena devices)',
parse: async (p) => parseAsInteger(p, 'dockerPort'),
parse: (p) => parseAsInteger(p, 'dockerPort'),
}),
};
@ -155,12 +156,48 @@ Can be repeated to add multiple certificates.\
------------------------------------------------------------------------------
`);
}
} catch (error) {
} catch {
throw new ExpectedError(
`The provided image path does not exist: ${params.image}`,
);
}
// Verify that image is not enabled for secure boot. First, confirm it is
// a secure boot image with a .sig file in the /opt directory of the rootA
// partition. For example, below are contents for generic-amd64 device type:
// $ ls -l opt
// total 864696
// -rw-r--r-- 1 root root 2378170368 Mar 26 09:14 balena-image-generic-amd64.balenaos-img
// -rw-r--r-- 1 root root 512 Mar 9 2018 balena-image-generic-amd64.balenaos-img.sig
const { explorePartition, BalenaPartition } = await import(
'../../utils/image-contents'
);
const isSecureBoot = await explorePartition<boolean>(
params.image,
BalenaPartition.ROOTA,
async (fs: typeof Fs): Promise<boolean> => {
try {
const files = await fs.promises.readdir('/opt');
return files.some((el) => el.endsWith('balenaos-img.sig'));
} catch {
// Typically one of:
// - Error: No such file or directory
// - Error: Unsupported filesystem.
// - ErrnoException: node_ext2fs_open ENOENT (44) args: [5261576,5268064,"r",0]
return false;
}
return false;
},
);
// Next verify that config.json enables secureboot.
if (isSecureBoot) {
const { read } = await import('balena-config-json');
const config = await read(params.image, '');
if (config.installer?.secureboot === true) {
throw new ExpectedError("Can't preload image with secure boot enabled");
}
}
// balena-preload currently does not work with numerical app IDs
// Load app here, and use app slug from hereon
const fleetSlug: string | undefined = options.fleet
@ -192,11 +229,11 @@ Can be repeated to add multiple certificates.\
event.name,
));
if (event.action === 'start') {
return spinner.start();
} else {
console.log();
return spinner.stop();
spinner.start();
return;
}
console.log();
spinner.stop();
};
const commit = this.isCurrentCommit(options.commit || '')
@ -295,7 +332,7 @@ Can be repeated to add multiple certificates.\
owns__release: {
$select: ['id', 'commit', 'end_timestamp', 'composition'],
$expand: {
contains__image: {
release_image: {
$select: ['image'],
$expand: {
image: {

View File

@ -24,7 +24,7 @@ import { tryAsInteger } from '../../utils/validation';
import { jsonInfo } from '../../utils/messages';
export const commitOrIdArg = Args.custom({
parse: async (commitOrId: string) => tryAsInteger(commitOrId),
parse: tryAsInteger,
});
type FlagsDef = Interfaces.InferredFlags<typeof ReleaseCmd.flags>;

View File

@ -34,7 +34,7 @@ export default class SSHKeyCmd extends Command {
public static args = {
id: Args.integer({
description: 'balenaCloud ID for the SSH key',
parse: async (x) => parseAsInteger(x, 'id'),
parse: (x) => parseAsInteger(x, 'id'),
required: true,
}),
};

View File

@ -40,7 +40,7 @@ export default class SSHKeyRmCmd extends Command {
public static args = {
id: Args.integer({
description: 'balenaCloud ID for the SSH key',
parse: async (x) => parseAsInteger(x, 'id'),
parse: (x) => parseAsInteger(x, 'id'),
required: true,
}),
};

View File

@ -69,10 +69,9 @@ export default class VersionCmd extends Command {
const { flags: options } = await this.parse(VersionCmd);
const versions: JsonVersions = {
'balena-cli': (await import('../../../package.json')).version,
'Node.js':
process.version && process.version.startsWith('v')
? process.version.slice(1)
: process.version,
'Node.js': process.version.startsWith('v')
? process.version.slice(1)
: process.version,
};
if (options.json) {
console.log(JSON.stringify(versions, null, 4));

View File

@ -93,7 +93,7 @@ function interpret(error: Error): string {
if (hasCode(error)) {
const errorCodeHandler = messages[error.code];
const message = errorCodeHandler && errorCodeHandler(error);
const message = errorCodeHandler?.(error);
if (message) {
return message;
@ -229,7 +229,7 @@ async function sentryCaptureException(error: Error) {
Sentry.captureException(error);
try {
await Sentry.close(1000);
} catch (e) {
} catch {
if (process.env.DEBUG) {
console.error('[debug] Timeout reporting error to sentry.io');
}

View File

@ -209,12 +209,12 @@ See: https://git.io/JRHUW#deprecation-policy`,
return indent(body, 2);
}
protected formatDescription(desc: string = '') {
protected formatDescription(desc = '') {
const chalk = getChalk();
desc = desc.split('\n')[0];
// Remove any ending .
if (desc[desc.length - 1] === '.') {
if (desc.endsWith('.')) {
desc = desc.substring(0, desc.length - 1);
}
// Lowercase first letter if second char is lowercase, to preserve e.g. 'SSH ...')

View File

@ -103,7 +103,7 @@ const hook: Hook<'prerun'> = async function (options) {
.offlineCompatible ?? DEFAULT_OFFLINE_COMPATIBLE
)
) {
await checkNotUsingOfflineMode();
checkNotUsingOfflineMode();
}
} catch (error) {
this.error(error);

View File

@ -48,7 +48,7 @@ export async function preparseArgs(argv: string[]): Promise<string[]> {
if (
cmdSlice.length > 1 &&
cmdSlice[0] === 'help' &&
cmdSlice[1][0] !== '-'
!cmdSlice[1].startsWith('-')
) {
cmdSlice.shift();
cmdSlice.push('--help');

View File

@ -164,9 +164,8 @@ export async function downloadOSImage(
stream.on('progress', (state: any) => {
if (state != null) {
return bar.update(state);
} else {
return spinner.start();
}
spinner.start();
});
stream.on('end', () => {

View File

@ -128,6 +128,7 @@ export const createRelease = async function (
draft: boolean,
semver: string | undefined,
contract: import('@balena/compose/dist/release/models').ReleaseModel['contract'],
imgDescriptors: ImageDescriptor[],
): Promise<Release> {
const _ = require('lodash') as typeof import('lodash');
const crypto = require('crypto') as typeof import('crypto');
@ -167,6 +168,7 @@ export const createRelease = async function (
semver,
is_final: !draft,
contract,
imgDescriptors,
});
return {
@ -240,7 +242,7 @@ export const getPreviousRepos = (
status: 'success',
},
$expand: {
contains__image: {
release_image: {
$select: 'image',
$expand: { image: { $select: 'is_stored_at__image_location' } },
},
@ -252,7 +254,7 @@ export const getPreviousRepos = (
.then(function (release) {
// grab all images from the latest release, return all image locations in the registry
if (release.length > 0) {
const images = release[0].contains__image as Array<{
const images = release[0].release_image as Array<{
image: [SDK.Image];
}>;
const { getRegistryAndName } =
@ -386,7 +388,7 @@ export class BuildProgressUI implements Renderer {
.map(function (service) {
const stream = through.obj(function (event, _enc, cb) {
eventHandler(service, event);
return cb();
cb();
});
stream.pipe(tty.stream, { end: false });
return [service, stream];
@ -471,17 +473,20 @@ export class BuildProgressUI implements Renderer {
const { status, progress, error } = serviceToDataMap[service] ?? {};
if (error) {
return `${error}`;
} else if (progress) {
}
if (progress) {
const bar = renderProgressBar(progress, 20);
if (status) {
return `${bar} ${status}`;
}
return `${bar}`;
} else if (status) {
return `${status}`;
} else {
return 'Waiting...';
return bar;
}
if (status) {
return status;
}
return 'Waiting...';
})
.map((data, index) => [services[index], data])
.fromPairs()
@ -552,7 +557,7 @@ export class BuildProgressInline implements Renderer {
.map(function (service) {
const stream = through.obj(function (event, _enc, cb) {
eventHandler(service, event);
return cb();
cb();
});
stream.pipe(outStream, { end: false });
return [service, stream];
@ -606,11 +611,11 @@ export class BuildProgressInline implements Renderer {
const { status, error } = event;
if (error) {
return `${error}`;
} else if (status) {
return `${status}`;
} else {
return 'Waiting...';
}
if (status) {
return status;
}
return 'Waiting...';
})();
const prefix = _.padEnd(getChalk().bold(service), this._prefixWidth);

View File

@ -966,7 +966,7 @@ export async function makeBuildTasks(
deviceInfo: DeviceInfo,
logger: Logger,
projectName: string,
releaseHash: string = 'unavailable',
releaseHash = 'unavailable',
preprocessHook?: (dockerfile: string) => string,
): Promise<MultiBuild.BuildTask[]> {
const multiBuild = await import('@balena/compose/dist/multibuild');
@ -1375,6 +1375,7 @@ export async function deployProject(
skipLogUpload: boolean,
projectPath: string,
isDraft: boolean,
imgDescriptors: ImageDescriptor[],
): Promise<import('@balena/compose/dist/release/models').ReleaseModel> {
const releaseMod = await import('@balena/compose/dist/release');
const { createRelease, tagServiceImages } = await import('./compose');
@ -1405,6 +1406,7 @@ export async function deployProject(
isDraft,
contract?.version,
contract,
imgDescriptors,
),
);
const { client: pineClient, release, serviceImages } = $release;
@ -1492,7 +1494,7 @@ export function createRunLoop(tick: (...args: any[]) => void) {
},
end() {
clearInterval(timerId);
return runloop.onEnd();
runloop.onEnd();
},
};
return runloop;
@ -1549,7 +1551,7 @@ function dropEmptyLinesStream() {
if (str.trim()) {
this.push(str);
}
return cb();
cb();
});
}
@ -1570,7 +1572,7 @@ function buildLogCapture(objectMode: boolean, buffer: string[]) {
buffer.push(data);
}
return cb(null, data);
cb(null, data);
});
}
@ -1585,14 +1587,16 @@ function buildProgressAdapter(inline: boolean) {
return through({ objectMode: true }, function (str, _enc, cb) {
if (str == null) {
return cb(null, str);
cb(null, str);
return;
}
if (inline) {
return cb(null, { status: str });
cb(null, { status: str });
return;
}
if (!/^Successfully tagged /.test(str)) {
if (!str.startsWith('Successfully tagged ')) {
const match = stepRegex.exec(str);
if (match) {
step = match[1];
@ -1607,7 +1611,7 @@ function buildProgressAdapter(inline: boolean) {
}
}
return cb(null, { status: str, progress });
cb(null, { status: str, progress });
});
}

View File

@ -14,7 +14,6 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import type * as BalenaSdk from 'balena-sdk';
import * as semver from 'balena-semver';
import { getBalenaSdk, stripIndent } from './lazy';
export interface ImgConfig {
@ -81,7 +80,7 @@ export async function generateApplicationConfig(
)) as ImgConfig;
// merge sshKeys to config, when they have been specified
if (options.os && options.os.sshKeys) {
if (options.os?.sshKeys) {
// Create config.os object if it does not exist
config.os = config.os ? config.os : {};
config.os.sshKeys = config.os.sshKeys
@ -122,16 +121,10 @@ export function generateDeviceConfig(
// os.getConfig always returns a config for an app
delete config.apiKey;
if (deviceApiKey == null && semver.satisfies(options.version, '<2.0.3')) {
config.apiKey = await sdk.models.application.generateApiKey(
application.id,
);
} else {
config.deviceApiKey =
typeof deviceApiKey === 'string' && deviceApiKey
? deviceApiKey
: await sdk.models.device.generateDeviceKey(device.uuid);
}
config.deviceApiKey =
typeof deviceApiKey === 'string' && deviceApiKey
? deviceApiKey
: await sdk.models.device.generateDeviceKey(device.uuid);
return config;
})

View File

@ -86,7 +86,7 @@ const uploadToPromise = (uploadRequest: Request, logger: Logger) =>
obj = JSON.parse(data);
} catch (e) {
logger.logError('Error parsing reply from remote side');
reject(e);
reject(e as Error);
return;
}
@ -232,7 +232,7 @@ export const deployLegacy = async function (
username,
appName,
]);
await uploadLogs(...args);
uploadLogs(...args);
}
return buildId;

View File

@ -74,7 +74,7 @@ export class DeviceAPI {
public constructor(
private logger: Logger,
addr: string,
port: number = 48484,
port = 48484,
) {
this.deviceAddress = `http://${addr}:${port}/`;
}
@ -201,7 +201,7 @@ export class DeviceAPI {
return new Promise((resolve, reject) => {
const req = request.get(url);
req.on('error', reject).on('response', async (res) => {
req.on('error', reject).on('response', (res) => {
if (res.statusCode !== 200) {
reject(
new ApiErrors.DeviceAPIError(
@ -213,7 +213,7 @@ export class DeviceAPI {
try {
res.socket.setKeepAlive(true, 1000);
} catch (error) {
reject(error);
reject(error as Error);
}
resolve(res);
});
@ -238,7 +238,7 @@ export class DeviceAPI {
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 as string;
url = (opts as ObjectWithUrl).url!;
} else if (typeof opts === 'string') {
url = opts;
}
@ -252,21 +252,26 @@ export class DeviceAPI {
return await new Promise((resolve, reject) => {
return request(opts, (err, response, body) => {
if (err) {
return reject(err);
reject(err as Error);
return;
}
switch (response.statusCode) {
case 200:
return resolve(body);
case 400:
return reject(
new ApiErrors.BadRequestDeviceAPIError(body.message),
);
case 503:
return reject(
new ApiErrors.ServiceUnavailableAPIError(body.message),
);
default:
return reject(new ApiErrors.DeviceAPIError(body.message));
case 200: {
resolve(body);
return;
}
case 400: {
reject(new ApiErrors.BadRequestDeviceAPIError(body.message));
return;
}
case 503: {
reject(new ApiErrors.ServiceUnavailableAPIError(body.message));
return;
}
default: {
reject(new ApiErrors.DeviceAPIError(body.message));
return;
}
}
});
});

View File

@ -74,11 +74,11 @@ interface ParsedEnvironment {
[serviceName: string]: { [key: string]: string };
}
async function environmentFromInput(
function environmentFromInput(
envs: string[],
serviceNames: string[],
logger: Logger,
): Promise<ParsedEnvironment> {
): ParsedEnvironment {
// A normal environment variable regex, with an added part
// to find a colon followed servicename at the start
const varRegex = /^(?:([^\s:]+):)?([^\s]+?)=(.*)$/;
@ -143,7 +143,7 @@ export async function deployToDevice(opts: DeviceDeployOptions): Promise<void> {
try {
globalLogger.logDebug('Checking we can access device');
await api.ping();
} catch (e) {
} catch {
throw new ExpectedError(stripIndent`
Could not communicate with device supervisor at address ${opts.deviceHost}:${port}.
Device may not have local mode enabled. Check with:
@ -191,10 +191,7 @@ export async function deployToDevice(opts: DeviceDeployOptions): Promise<void> {
});
// Attempt to attach to the device's docker daemon
const docker = connectToDocker(
opts.deviceHost,
opts.devicePort != null ? opts.devicePort : 2375,
);
const docker = connectToDocker(opts.deviceHost, opts.devicePort ?? 2375);
await checkBuildSecretsRequirements(docker, opts.source);
globalLogger.logDebug('Tarring all non-ignored files...');
@ -231,7 +228,7 @@ export async function deployToDevice(opts: DeviceDeployOptions): Promise<void> {
// Print a newline to clearly separate build time and runtime
console.log();
const envs = await environmentFromInput(
const envs = environmentFromInput(
opts.env,
Object.getOwnPropertyNames(project.composition.services),
globalLogger,
@ -388,7 +385,7 @@ async function performBuilds(
);
// Check for failures
await inspectBuildResults(localImages);
inspectBuildResults(localImages);
const imagesToRemove: string[] = [];
@ -497,7 +494,7 @@ export async function rebuildSingleTask(
}
await assignDockerBuildOpts(docker, [task], opts);
await assignOutputHandlers([task], logger, logHandler);
assignOutputHandlers([task], logger, logHandler);
const [localImage] = await multibuild.performBuilds(
[task],
@ -568,7 +565,7 @@ async function assignDockerBuildOpts(
globalLogger.logDebug(`Using ${images.length} on-device images for cache...`);
await Promise.all(
buildTasks.map(async (task: BuildTask) => {
buildTasks.map((task: BuildTask) => {
task.dockerOpts = {
...(task.dockerOpts || {}),
...{
@ -666,7 +663,7 @@ export function generateTargetState(
return targetState;
}
async function inspectBuildResults(images: LocalImage[]): Promise<void> {
function inspectBuildResults(images: LocalImage[]): void {
const failures: LocalPushErrors.BuildFailure[] = [];
_.each(images, (image) => {
@ -679,6 +676,6 @@ async function inspectBuildResults(images: LocalImage[]): Promise<void> {
});
if (failures.length > 0) {
throw new LocalPushErrors.BuildError(failures).toString();
throw new LocalPushErrors.BuildError(failures);
}
}

View File

@ -191,8 +191,8 @@ export class LivepushManager {
);
const eventQueue = this.updateEventsWaiting[$serviceName];
eventQueue.push(changedPath);
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.getDebouncedEventHandler($serviceName)();
void this.getDebouncedEventHandler($serviceName)();
};
const monitor = this.setupFilesystemWatcher(
@ -252,7 +252,7 @@ export class LivepushManager {
try {
// sync because chokidar defines a sync interface
stats = fs.lstatSync(filePath);
} catch (err) {
} catch {
// OK: the file may have been deleted. See also:
// https://github.com/paulmillr/chokidar/blob/3.4.3/lib/fsevents-handler.js#L326-L328
// https://github.com/paulmillr/chokidar/blob/3.4.3/lib/nodefs-handler.js#L364
@ -267,15 +267,15 @@ export class LivepushManager {
return dockerignore.ignores(relPath);
},
});
monitor.on('add', (changedPath: string) =>
changedPathHandler(serviceName, changedPath),
);
monitor.on('change', (changedPath: string) =>
changedPathHandler(serviceName, changedPath),
);
monitor.on('unlink', (changedPath: string) =>
changedPathHandler(serviceName, changedPath),
);
monitor.on('add', (changedPath: string) => {
changedPathHandler(serviceName, changedPath);
});
monitor.on('change', (changedPath: string) => {
changedPathHandler(serviceName, changedPath);
});
monitor.on('unlink', (changedPath: string) => {
changedPathHandler(serviceName, changedPath);
});
return monitor;
}

View File

@ -57,7 +57,7 @@ export const dockerConnectionCliFlags = {
description:
'Docker daemon TCP port number (hint: 2375 for balena devices)',
char: 'p',
parse: async (p) => parseAsInteger(p, 'dockerPort'),
parse: (p) => parseAsInteger(p, 'dockerPort'),
}),
ca: Flags.string({
description: 'Docker host TLS certificate authority file',
@ -169,9 +169,7 @@ export async function isBalenaEngine(docker: dockerode): Promise<boolean> {
// version of balenaEngine, but it was at one point (mis)spelt 'balaena':
// https://github.com/balena-os/balena-engine/pull/32/files
const dockerVersion = (await docker.version()) as BalenaEngineVersion;
return !!(
dockerVersion.Engine && dockerVersion.Engine.match(/balena|balaena/)
);
return !!dockerVersion.Engine?.match(/balena|balaena/);
}
export async function getDocker(

View File

@ -84,7 +84,7 @@ export async function readFileWithEolConversion(
}
// Analyse encoding
const encoding = await detectEncoding(fileBuffer);
const encoding = detectEncoding(fileBuffer);
// Skip further processing of non-convertible encodings
if (!CONVERTIBLE_ENCODINGS.includes(encoding)) {
@ -132,10 +132,10 @@ export async function readFileWithEolConversion(
* @param fileBuffer File contents whose encoding should be detected
* @param bytesRead Optional "file size" if smaller than the buffer size
*/
export async function detectEncoding(
export function detectEncoding(
fileBuffer: Buffer,
bytesRead = fileBuffer.length,
): Promise<string> {
): string {
// empty file
if (bytesRead === 0) {
return '';

View File

@ -110,6 +110,27 @@ export async function getManifest(
const init = await import('balena-device-init');
const sdk = getBalenaSdk();
const manifest = await init.getImageManifest(image);
if (manifest != null) {
const config = manifest.configuration?.config;
if (config?.partition != null) {
const { getBootPartition } = await import('balena-config-json');
// Find the device-type.json property that holds the boot partition number for
// this device type (config.partition or config.partition.primary) and overwrite it
// with the boot partition number that was found by inspecting the image.
// since it's deprecated & no longer updated for newer releases.
if (typeof config.partition === 'number') {
config.partition = await getBootPartition(image);
} else if (config.partition.primary != null) {
config.partition.primary = await getBootPartition(image);
}
// TODO: Add handling for when we no longer include a `config.partition` at all.
}
} else {
// TODO: Change this in the next major to throw, after confirming that this works for all supported OS versions.
console.error(
`[warn] Error while finding a device-type.json on the provided image path. Attempting to fetch from the API.`,
);
}
if (
manifest != null &&
manifest.slug !== deviceType &&
@ -281,8 +302,7 @@ export function isWindowsComExeShell() {
// neither bash nor sh (e.g. not MSYS, MSYS2, Cygwin, WSL)
process.env.SHELL == null &&
// Windows cmd.exe or PowerShell
process.env.ComSpec != null &&
process.env.ComSpec.endsWith('cmd.exe')
process.env.ComSpec?.endsWith('cmd.exe')
);
}
@ -366,7 +386,7 @@ export function getProxyConfig(): ProxyConfig | undefined {
let url: InstanceType<typeof URL>;
try {
url = new URL(proxyUrl);
} catch (_e) {
} catch {
return;
}
return {
@ -469,7 +489,7 @@ export function pickAndRename<T extends Dictionary<any>>(
let renameFrom = f;
let renameTo = f;
const match = f.match(/(?<from>\S+)\s+=>\s+(?<to>\S+)/);
if (match && match.groups) {
if (match?.groups) {
renameFrom = match.groups.from;
renameTo = match.groups.to;
}

View File

@ -0,0 +1,69 @@
/**
* @license
* Copyright 2025 Balena Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// Utilities to explore the contents in a balenaOS image.
import * as imagefs from 'balena-image-fs';
import * as filedisk from 'file-disk';
import { getPartitions } from 'partitioninfo';
import type * as Fs from 'fs';
/**
* @summary IDs for the standard balenaOS partitions
* @description Values are the base name for a partition on disk
*/
export enum BalenaPartition {
BOOT = 'boot',
ROOTA = 'rootA',
ROOTB = 'rootB',
STATE = 'state',
DATA = 'data',
}
/**
* @summary Allow a provided function to explore the contents of one of the well-known
* partitions of a balenaOS image
*
* @param {string} imagePath - pathname of image for search
* @param {BalenaPartition} partitionId - partition to find
* @param {(fs) => Promise<T>} - function for exploration
* @returns {T}
*/
export async function explorePartition<T>(
imagePath: string,
partitionId: BalenaPartition,
exploreFn: (fs: typeof Fs) => Promise<T>,
): Promise<T> {
return await filedisk.withOpenFile(imagePath, 'r', async (handle) => {
const disk = new filedisk.FileDisk(handle, true, false, false);
const partitionInfo = await getPartitions(disk, {
includeExtended: false,
getLogical: true,
});
const findResult = await imagefs.findPartition(disk, partitionInfo, [
`resin-${partitionId}`,
`flash-${partitionId}`,
`balena-${partitionId}`,
]);
if (findResult == null) {
throw new Error(`Can't find partition for ${partitionId}`);
}
return await imagefs.interact<T>(disk, findResult.index, exploreFn);
});
}

View File

@ -76,38 +76,28 @@ export const getImagePath = async (deviceType: string, version?: string) => {
};
/**
* @summary Determine if a device image is fresh
* @summary Determine if a device image is cached
*
* @description
* If the device image does not exist, return false.
*
* @param {String} deviceType - device type slug or alias
* @param {String} version - the exact balenaOS version number
* @returns {Promise<Boolean>} is image fresh
* @returns {Promise<Boolean>} is image cached
*
* @example
* isImageFresh('raspberry-pi', '1.2.3').then (isFresh) ->
* if isFresh
* console.log('The Raspberry Pi image v1.2.3 is fresh!')
* isImageCached ('raspberry-pi', '1.2.3').then (isCached) ->
* if isCached
* console.log('The Raspberry Pi image v1.2.3 is cached!')
*/
export const isImageFresh = async (deviceType: string, version: string) => {
export const isImageCached = async (deviceType: string, version: string) => {
const imagePath = await getImagePath(deviceType, version);
let createdDate;
try {
createdDate = await getFileCreatedDate(imagePath);
const createdDate = await getFileCreatedDate(imagePath);
return createdDate != null;
} catch {
// Swallow errors from getFileCreatedTime.
}
if (createdDate == null) {
return false;
}
const balena = getBalenaSdk();
const lastModifiedDate = await balena.models.os.getLastModified(
deviceType,
version,
);
return lastModifiedDate < createdDate;
};
/**
@ -118,7 +108,7 @@ export const isImageFresh = async (deviceType: string, version: string) => {
*/
export const isESR = (version: string) => {
const match = version.match(/^v?(\d+)\.\d+\.\d+/);
const major = parseInt((match && match[1]) || '', 10);
const major = parseInt(match?.[1] || '', 10);
return major >= 2018; // note: (NaN >= 2018) is false
};
@ -286,7 +276,7 @@ export const getStream = async (
versionOrRange = 'latest';
}
const version = await resolveVersion(deviceType, versionOrRange);
const isFresh = await isImageFresh(deviceType, version);
const isFresh = await isImageCached(deviceType, version);
const $stream = isFresh
? await getImage(deviceType, version)
: await doDownload({ ...options, deviceType, version });

View File

@ -21,6 +21,7 @@ import type { Chalk } from 'chalk';
import type * as visuals from 'resin-cli-visuals';
import type * as CliForm from 'resin-cli-form';
import type { ux } from '@oclif/core';
import { version } from '../../package.json';
// Equivalent of _.once but avoiding the need to import lodash for lazy deps
const once = <T>(fn: () => T) => {
@ -43,9 +44,26 @@ export const onceAsync = <T>(fn: () => Promise<T>) => {
};
};
export const getBalenaSdk = once(() =>
(require('balena-sdk') as typeof BalenaSdk).fromSharedOptions(),
);
const cliXBalenaClientHeaderInterceptor: BalenaSdk.Interceptor = {
request($request) {
if ($request.headers['X-Balena-Client']) {
// We intentionally overwrite the sdk version string from the header
// to conserve bandwidth. We only do that when the SDK already has specified
// the X-Balena-Client header, since that signals that this is a safe url to
// include the extra header and will not cause CORS errors.
$request.headers['X-Balena-Client'] = `balena-cli/${version}`;
}
return $request;
},
};
export const getBalenaSdk = once(() => {
const sdk = (require('balena-sdk') as typeof BalenaSdk).fromSharedOptions();
if (!sdk.interceptors.includes(cliXBalenaClientHeaderInterceptor)) {
sdk.interceptors.push(cliXBalenaClientHeaderInterceptor);
}
return sdk;
});
export const getVisuals = once(
() => require('resin-cli-visuals') as typeof visuals,

View File

@ -39,7 +39,7 @@ export async function disambiguateReleaseParam(
// Accepting short hashes of 7,8,9 chars.
const possibleUuidHashLength = [7, 8, 9, 32, 40, 62].includes(release.length);
const hasLeadingZero = release[0] === '0';
const hasLeadingZero = release.startsWith('0');
const isOnlyNumerical = /^[0-9]+$/.test(release);
// Reject non-numerical values with invalid uuid/hash lengths
@ -75,17 +75,19 @@ export async function disambiguateReleaseParam(
return (await balena.models.release.get(release, { $select: 'id' })).id;
}
/* eslint-disable @typescript-eslint/require-await -- oclif parse functions require a Promise return */
/**
* Convert to lowercase if looks like slug
*/
export async function lowercaseIfSlug(s: string) {
/* eslint-enable @typescript-eslint/require-await */
return s.includes('/') ? s.toLowerCase() : s;
}
export function normalizeOsVersion(version: string) {
// Note that `version` may also be 'latest', 'recommended', 'default'
if (/^v?\d+\.\d+\.\d+/.test(version)) {
if (version[0] === 'v') {
if (version.startsWith('v')) {
version = version.slice(1);
}
}

View File

@ -25,7 +25,7 @@ export function capitanoizeOclifUsage(
.toLowerCase();
}
export async function getCommandsFromManifest() {
export function getCommandsFromManifest() {
const manifest = require('../../oclif.manifest.json');
if (manifest.commands == null) {

View File

@ -119,7 +119,7 @@ export const checkLoggedInIf = async (doCheck: boolean) => {
*
* @throws {NotAvailableInOfflineModeError}
*/
export const checkNotUsingOfflineMode = async () => {
export const checkNotUsingOfflineMode = () => {
if (process.env.BALENARC_OFFLINE_MODE) {
throw new NotAvailableInOfflineModeError(stripIndent`
This command requires an internet connection, and cannot be used in offline mode.

View File

@ -390,13 +390,12 @@ async function createApplication(
try {
const userInfo = await sdk.auth.getUserInfo();
username = userInfo.username;
} catch (err) {
} catch {
throw new sdk.errors.BalenaNotLoggedIn();
}
// eslint-disable-next-line no-async-promise-executor
const applicationName = await new Promise<string>(async (resolve, reject) => {
// eslint-disable-next-line no-constant-condition
while (true) {
try {
const appName = await getCliForm().ask({
@ -418,11 +417,13 @@ async function createApplication(
'You already have a fleet with that name; please choose another.',
);
continue;
} catch (err) {
return resolve(appName);
} catch {
resolve(appName);
return;
}
} catch (err) {
return reject(err);
reject(err as Error);
return;
}
}
});
@ -452,9 +453,7 @@ async function generateApplicationConfig(
const manifest = await sdk.models.config.getDeviceTypeManifestBySlug(
app.is_for__device_type[0].slug,
);
const opts =
manifest.options &&
manifest.options.filter((opt) => opt.name !== 'network');
const opts = manifest.options?.filter((opt) => opt.name !== 'network');
const override = {
appUpdatePollInterval: options.appUpdatePollInterval,

View File

@ -50,7 +50,7 @@ export function copyQemu(context: string, arch: string) {
.then(() => getQemuPath(arch))
.then(
(qemu) =>
new Promise(function (resolve, reject) {
new Promise<void>(function (resolve, reject) {
const read = fs.createReadStream(qemu);
const write = fs.createWriteStream(binPath);
@ -114,7 +114,7 @@ async function installQemu(arch: string, qemuPath: string) {
stream.resume();
}
} catch (err) {
reject(err);
reject(err as Error);
}
});
request(qemuUrl)

View File

@ -110,7 +110,7 @@ export async function startRemoteBuild(
const [buildRequest, stream] = await getRemoteBuildStream(build);
// Setup CTRL-C handler so the user can interrupt the build
let cancellationPromise = Promise.resolve();
let cancellationPromise: Promise<void> | undefined;
const sigintHandler = () => {
process.exitCode = 130;
console.error('\nReceived SIGINT, cleaning up. Please wait.');
@ -246,7 +246,8 @@ function getBuilderMessageHandler(
console.error(`[debug] handling message: ${JSON.stringify(obj)}`);
}
if (obj.type != null && obj.type === 'metadata') {
return handleBuilderMetadata(obj, build);
handleBuilderMetadata(obj, build);
return;
}
if (obj.message) {
readline.clearLine(process.stdout, 0);
@ -423,10 +424,20 @@ async function getRemoteBuildStream(
stream = buildRequest.pipe(JSONStream.parse('*')) as NodeJS.ReadStream;
}
stream = stream
.once('error', () => uploadSpinner.stop())
.once('close', () => uploadSpinner.stop())
.once('data', () => uploadSpinner.stop())
.once('end', () => uploadSpinner.stop())
.once('finish', () => uploadSpinner.stop());
.once('error', () => {
uploadSpinner.stop();
})
.once('close', () => {
uploadSpinner.stop();
})
.once('data', () => {
uploadSpinner.stop();
})
.once('end', () => {
uploadSpinner.stop();
})
.once('finish', () => {
uploadSpinner.stop();
});
return [buildRequest, stream];
}

View File

@ -73,7 +73,7 @@ export function sshArgsForRemoteCommand({
...['-o', 'LogLevel=ERROR'],
...['-o', 'StrictHostKeyChecking=no'],
...['-o', 'UserKnownHostsFile=/dev/null'],
...(proxyCommand && proxyCommand.length
...(proxyCommand?.length
? ['-o', `ProxyCommand=${proxyCommand.join(' ')}`]
: []),
`${username}@${hostname}`,
@ -155,9 +155,9 @@ export async function runRemoteCommand({
[exitCode, exitSignal] = await new Promise((resolve, reject) => {
const ps = spawn(program, args, { stdio })
.on('error', reject)
.on('close', (code, signal) =>
resolve([code ?? undefined, signal ?? undefined]),
);
.on('close', (code, signal) => {
resolve([code ?? undefined, signal ?? undefined]);
});
if (ps.stdin && stdin && typeof stdin !== 'string') {
stdin.pipe(ps.stdin);
@ -272,7 +272,7 @@ export async function getLocalDeviceCmdStdout(
export const isRootUserGood = _.memoize(async (hostname: string, port) => {
try {
await runRemoteCommand({ cmd: 'exit 0', hostname, port, ...stdioIgnore });
} catch (e) {
} catch {
return false;
}
return true;

View File

@ -29,7 +29,11 @@ export function buffer(
new Promise(function (resolve, reject) {
const fstream = fs.createReadStream(bufferFile);
fstream.on('open', () => resolve(fstream)).on('error', reject);
fstream
.on('open', () => {
resolve(fstream);
})
.on('error', reject);
}),
);
}

View File

@ -105,7 +105,7 @@ async function spawnAndPipe(
});
}
async function windosuExec(
function windosuExec(
escapedArgs: string[],
stderr?: NodeJS.WritableStream,
): Promise<void> {

View File

@ -42,9 +42,9 @@ export = (stream: NodeJS.WriteStream = process.stdout) => {
const showCursor = () => stream.write('\u001B[?25h');
const cursorUp = (rows: number = 0) => stream.write(`\u001B[${rows}A`);
const cursorUp = (rows = 0) => stream.write(`\u001B[${rows}A`);
const cursorDown = (rows: number = 0) => stream.write(`\u001B[${rows}B`);
const cursorDown = (rows = 0) => stream.write(`\u001B[${rows}B`);
const write = (str: string) => stream.write(str);

View File

@ -61,11 +61,11 @@ export const tunnelConnectionToDevice = (
client.pipe(remote);
remote.pipe(client);
remote.on('error', (err) => {
console.error('Remote: ' + err);
console.error(`Remote: ${err}`);
client.end();
});
client.on('error', (err) => {
console.error('Client: ' + err);
console.error(`Client: ${err}`);
remote.end();
});
remote.on('close', () => {

View File

@ -87,7 +87,8 @@ export function looksLikeInteger(input: string) {
return /^(?:0|[1-9][0-9]*)$/.test(input);
}
export function parseAsInteger(input: string, paramName?: string) {
// eslint-disable-next-line @typescript-eslint/require-await -- oclif parse functions require a Promise return
export async function parseAsInteger(input: string, paramName?: string) {
if (!looksLikeInteger(input)) {
const message =
paramName == null
@ -100,14 +101,15 @@ export function parseAsInteger(input: string, paramName?: string) {
return Number(input);
}
export function tryAsInteger(input: string): number | string {
export async function tryAsInteger(input: string): Promise<number | string> {
try {
return parseAsInteger(input);
return await parseAsInteger(input);
} catch {
return input;
}
}
// eslint-disable-next-line @typescript-eslint/require-await -- oclif parse functions require a Promise return
export async function parseAsLocalHostnameOrIp(input: string) {
if (input && !validateLocalHostnameOrIp(input)) {
throw new ExpectedError(

View File

@ -31,7 +31,7 @@ chai.use(chaiAsPromised);
const { expect } = chai;
async function getPage(name: string): Promise<string> {
function getPage(name: string): string {
const pagePath = path.join(
__dirname,
'..',
@ -88,7 +88,7 @@ describe('Login server:', function () {
expect(body).to.equal(opt.expectedBody);
resolve();
} catch (err) {
reject(err);
reject(err as Error);
}
},
);
@ -134,7 +134,7 @@ describe('Login server:', function () {
describe('given the token authenticates with the server', function () {
beforeEach(function () {
this.loginIfTokenValidStub = sinon.stub(utils, 'loginIfTokenValid');
this.loginIfTokenValidStub.returns(Promise.resolve(true));
this.loginIfTokenValidStub.resolves(true);
});
afterEach(function () {
@ -143,7 +143,7 @@ describe('Login server:', function () {
it('should eventually be the token', async () => {
await testLogin({
expectedBody: await getPage('success'),
expectedBody: getPage('success'),
expectedStatusCode: 200,
expectedToken: tokens.johndoe.token,
});
@ -153,7 +153,7 @@ describe('Login server:', function () {
describe('given the token does not authenticate with the server', function () {
beforeEach(function () {
this.loginIfTokenValidStub = sinon.stub(utils, 'loginIfTokenValid');
return this.loginIfTokenValidStub.returns(Promise.resolve(false));
return this.loginIfTokenValidStub.resolves(false);
});
afterEach(function () {
@ -162,7 +162,7 @@ describe('Login server:', function () {
it('should be rejected', async () => {
await testLogin({
expectedBody: await getPage('error'),
expectedBody: getPage('error'),
expectedStatusCode: 401,
expectedToken: tokens.johndoe.token,
expectedErrorMsg: 'Invalid token',
@ -171,7 +171,7 @@ describe('Login server:', function () {
it('should be rejected if no token', async () => {
await testLogin({
expectedBody: await getPage('error'),
expectedBody: getPage('error'),
expectedStatusCode: 401,
expectedToken: '',
expectedErrorMsg: 'No token',
@ -180,7 +180,7 @@ describe('Login server:', function () {
it('should be rejected if token is malformed', async () => {
await testLogin({
expectedBody: await getPage('error'),
expectedBody: getPage('error'),
expectedStatusCode: 401,
expectedToken: 'asdf',
expectedErrorMsg: 'Invalid token',

View File

@ -267,15 +267,15 @@ describe('balena build', function () {
...fsMod,
promises: {
...fsMod.promises,
access: async (p: string) =>
access: (p: string) =>
p === qemuBinPath ? undefined : fsMod.promises.access(p),
stat: async (p: string) =>
stat: (p: string) =>
p === qemuBinPath ? { size: 1 } : fsMod.promises.stat(p),
},
});
mock(qemuModPath, {
...qemuMod,
copyQemu: async () => '',
copyQemu: () => '',
});
mock.reRequire('../../build/utils/qemu');
docker.expectGetInfo({ OperatingSystem: 'balenaOS 2.44.0+rev1' });

View File

@ -114,6 +114,14 @@ describe('balena device', function () {
'Content-Type': 'application/json',
});
api.scope
.get(
/^\/v\d+\/device\?.+&\$select=overall_status,overall_progress,should_be_running__release$/,
)
.replyWithFile(200, path.join(apiResponsePath, 'device.json'), {
'Content-Type': 'application/json',
});
const { out, err } = await runCommand('device 27fda508c --json');
expect(err).to.be.empty;
const json = JSON.parse(out.join(''));

View File

@ -169,8 +169,12 @@ async function startMockSshServer(): Promise<[Server, number]> {
console.error(`[debug] mock ssh server: ${msg}`);
}
};
c.on('error', (err) => handler(err.message));
c.on('end', () => handler('client disconnected'));
c.on('error', (err) => {
handler(err.message);
});
c.on('end', () => {
handler('client disconnected');
});
c.end();
});
server.on('error', (err) => {

View File

@ -21,6 +21,8 @@ import * as process from 'process';
import { runCommand } from '../../helpers';
import { promisify } from 'util';
import * as tmp from 'tmp';
import type * as $imagefs from 'balena-image-fs';
import * as stripIndent from 'common-tags/lib/stripIndent';
tmp.setGracefulCleanup();
const tmpNameAsync = promisify(tmp.tmpName);
@ -29,30 +31,94 @@ import { BalenaAPIMock } from '../../nock/balena-api-mock';
if (process.platform !== 'win32') {
describe('balena os configure', function () {
let imagefs: typeof $imagefs;
let api: BalenaAPIMock;
let tmpPath: string;
let tmpDummyPath: string;
let tmpMatchingDtJsonPartitionPath: string;
let tmpNonMatchingDtJsonPartitionPath: string;
beforeEach(async () => {
before(async function () {
// We conditionally import balena-image-fs, since when imported on top level then unrelated tests on win32 failed with:
// EPERM: operation not permitted, rename 'C:\Users\RUNNER~1\AppData\Local\Temp\tmp-<...>.inprogress' -> 'C:\Users\RUNNER~1\AppData\Local\Temp\tmp-<...>'
// at async Object.rename (node:internal/fs/promises:782:10) {
imagefs = await import('balena-image-fs');
tmpDummyPath = (await tmpNameAsync()) as string;
await fs.copyFile('./tests/test-data/dummy.img', tmpDummyPath);
tmpMatchingDtJsonPartitionPath = (await tmpNameAsync()) as string;
await fs.copyFile(
'./tests/test-data/mock-jetson-nano-6.0.13.with-boot-partition-12.img',
tmpMatchingDtJsonPartitionPath,
);
tmpNonMatchingDtJsonPartitionPath = (await tmpNameAsync()) as string;
// Create an image with a device-type.json that mentions a non matching boot partition.
// We copy the pre-existing image and modify it, since including a separate one
// would add 18MB more to the repository.
await fs.copyFile(
'./tests/test-data/mock-jetson-nano-6.0.13.with-boot-partition-12.img',
tmpNonMatchingDtJsonPartitionPath,
);
await imagefs.interact(
tmpNonMatchingDtJsonPartitionPath,
12,
async (_fs) => {
const dtJson = JSON.parse(
await _fs.promises.readFile('/device-type.json', {
encoding: 'utf8',
}),
);
expect(dtJson).to.have.nested.property(
'configuration.config.partition',
12,
);
dtJson.configuration.config.partition = 999;
await _fs.promises.writeFile(
'/device-type.json',
JSON.stringify(dtJson),
);
await _fs.promises.writeFile(
'/os-release',
stripIndent`
ID="balena-os"
NAME="balenaOS"
VERSION="6.1.25"
VERSION_ID="6.1.25"
PRETTY_NAME="balenaOS 6.1.25"
DISTRO_CODENAME="kirkstone"
MACHINE="jetson-nano"
META_BALENA_VERSION="6.1.25"`,
);
},
);
});
beforeEach(() => {
api = new BalenaAPIMock();
api.expectGetWhoAmI({ optional: true, persist: true });
tmpPath = (await tmpNameAsync()) as string;
await fs.copyFile('./tests/test-data/dummy.img', tmpPath);
});
afterEach(async () => {
afterEach(() => {
api.done();
await fs.unlink(tmpPath);
});
it('should inject a valid config.json file', async () => {
after(async () => {
await fs.unlink(tmpDummyPath);
await fs.unlink(tmpMatchingDtJsonPartitionPath);
await fs.unlink(tmpNonMatchingDtJsonPartitionPath);
});
it('should detect the OS version and inject a valid config.json file to a 6.0.13 image with partition 12 as boot & matching device-type.json', async () => {
api.expectGetApplication();
api.expectGetConfigDeviceTypes();
api.expectGetDeviceTypes();
// It should not reach to /config or /device-types/v1 but instead find
// everything required from the device-type.json in the image.
// api.expectGetConfigDeviceTypes();
api.expectDownloadConfig();
const command: string[] = [
`os configure ${tmpPath}`,
'--device-type raspberrypi3',
'--version 2.47.0+rev1',
`os configure ${tmpMatchingDtJsonPartitionPath}`,
'--device-type jetson-nano',
'--fleet testApp',
'--config-app-update-poll-interval 10',
'--config-network ethernet',
@ -65,9 +131,124 @@ if (process.platform !== 'win32') {
expect(err.join('')).to.equal('');
// confirm the image contains a config.json...
const imagefs = await import('balena-image-fs');
const config = await imagefs.interact(tmpPath, 1, async (_fs) => {
return await promisify(_fs.readFile)('/config.json');
const config = await imagefs.interact(
tmpMatchingDtJsonPartitionPath,
12,
async (_fs) => {
const dtJson = JSON.parse(
await _fs.promises.readFile('/device-type.json', {
encoding: 'utf8',
}),
);
// confirm that the device-type.json mentions the expected partition
expect(dtJson).to.have.nested.property(
'configuration.config.partition',
12,
);
return await _fs.promises.readFile('/config.json');
},
);
expect(config).to.not.be.empty;
// confirm the image has the correct config.json values...
const configObj = JSON.parse(config.toString('utf8'));
expect(configObj).to.have.property('deviceType', 'jetson-nano');
expect(configObj).to.have.property('initialDeviceName', 'testDeviceName');
});
it('should detect the OS version and inject a valid config.json file to a 6.1.25 image with partition 12 as boot & a non-matching device-type.json', async () => {
api.expectGetApplication();
api.expectGetDeviceTypes();
// It should not reach to /config or /device-types/v1 but instead find
// everything required from the device-type.json in the image.
// api.expectGetConfigDeviceTypes();
api.expectDownloadConfig();
const command: string[] = [
`os configure ${tmpNonMatchingDtJsonPartitionPath}`,
'--device-type jetson-nano',
'--fleet testApp',
'--config-app-update-poll-interval 10',
'--config-network ethernet',
'--initial-device-name testDeviceName',
'--provisioning-key-name testKey',
'--provisioning-key-expiry-date 2050-12-12',
];
const { err } = await runCommand(command.join(' '));
expect(err.join('')).to.equal('');
// confirm the image contains a config.json...
const config = await imagefs.interact(
tmpNonMatchingDtJsonPartitionPath,
12,
async (_fs) => {
const dtJson = JSON.parse(
await _fs.promises.readFile('/device-type.json', {
encoding: 'utf8',
}),
);
// confirm that the device-type.json mentions the expected partition
expect(dtJson).to.have.nested.property(
'configuration.config.partition',
999,
);
return await _fs.promises.readFile('/config.json');
},
);
expect(config).to.not.be.empty;
// confirm the image has the correct config.json values...
const configObj = JSON.parse(config.toString('utf8'));
expect(configObj).to.have.property('deviceType', 'jetson-nano');
expect(configObj).to.have.property('initialDeviceName', 'testDeviceName');
});
// TODO: In the next major consider just failing when we can't find a device-types.json in the image.
it('should inject a valid config.json file to a dummy image', async () => {
api.expectGetApplication();
// Since the dummy image doesn't include a device-type.json
// we have to reach to the API to fetch the manifest of the device type.
api.expectGetConfigDeviceTypes();
api.expectDownloadConfig();
const command: string[] = [
`os configure ${tmpDummyPath}`,
'--device-type raspberrypi3',
'--version 2.47.0+rev1',
'--fleet testApp',
'--config-app-update-poll-interval 10',
'--config-network ethernet',
'--initial-device-name testDeviceName',
'--provisioning-key-name testKey',
'--provisioning-key-expiry-date 2050-12-12',
];
const { err } = await runCommand(command.join(' '));
// Once we replace the dummy.img with one that includes a os-release & device-type.json
// then we should be able to change this to expect no errors.
expect(
err.flatMap((line) => line.split('\n')).filter((line) => line !== ''),
).to.deep.equal(
stripIndent`
[warn] "${tmpDummyPath}":
[warn] Found partition table with 1 partitions,
[warn] but none with a name/label in ['resin-boot', 'flash-boot', 'balena-boot'].
[warn] Will scan all partitions for contents.
[warn] "${tmpDummyPath}":
[warn] 1 partition(s) found, but none containing file "/device-type.json".
[warn] Assuming default boot partition number '1'.
[warn] "${tmpDummyPath}":
[warn] Could not find a previous "/config.json" file in partition '1'.
[warn] Proceeding anyway, but this is unexpected.
[warn] Error while finding a device-type.json on the provided image path. Attempting to fetch from the API.`.split(
'\n',
),
);
// confirm the image contains a config.json...
const config = await imagefs.interact(tmpDummyPath, 1, async (_fs) => {
return await _fs.promises.readFile('/config.json');
});
expect(config).to.not.be.empty;

View File

@ -462,7 +462,13 @@ describe('balena push', function () {
tmp.setGracefulCleanup();
const projectPath = await new Promise<string>((resolve, reject) => {
const opts = { template: 'tmp-XXXXXX', unsafeCleanup: true };
tmp.dir(opts, (e, p) => (e ? reject(e) : resolve(p)));
tmp.dir(opts, (e, p) => {
if (e) {
reject(e);
} else {
resolve(p);
}
});
});
console.error(`[debug] Temp project dir: ${projectPath}`);
@ -475,7 +481,7 @@ describe('balena push', function () {
try {
server.listen(socketPath, resolve);
} catch (e) {
reject(e);
reject(e as Error);
}
});
console.error(`[debug] Checking existence of socket at '${socketPath}'`);
@ -505,7 +511,13 @@ describe('balena push', function () {
// Terminate Unix Domain Socket server
await new Promise<void>((resolve, reject) => {
server.close((e) => (e ? reject(e) : resolve()));
server.close((e) => {
if (e) {
reject(e);
} else {
resolve();
}
});
});
expect(await exists(socketPath), 'Socket existence').to.be.false;

View File

@ -82,7 +82,7 @@ describe('balena release', function () {
expect(err).to.be.empty;
const json = JSON.parse(out.join(''));
expect(json[0].commit).to.equal('90247b54de4fa7a0a3cbc85e73c68039');
expect(json[0].contains__image[0].image[0].start_timestamp).to.equal(
expect(json[0].release_image[0].image[0].start_timestamp).to.equal(
'2020-01-04T01:13:08.583Z',
);
});

View File

@ -26,7 +26,7 @@ describe('balena whoami', function () {
api = new BalenaAPIMock();
});
this.afterEach(async () => {
this.afterEach(() => {
// Check all expected api calls have been made and clean up.
api.done();
});

View File

@ -26,7 +26,6 @@ import * as tar from 'tar-stream';
import { streamToBuffer } from 'tar-utils';
import { URL } from 'url';
import { diff } from 'deep-object-diff';
import { makeImageName } from '../build/utils/compose_ts';
import { stripIndent } from '../build/utils/lazy';
import type { BuilderMock } from './nock/builder-mock';
@ -77,13 +76,13 @@ export async function inspectTarStream(
type: header.type,
};
const expected = expectedFiles[header.name];
if (expected && expected.testStream) {
if (expected?.testStream) {
await expected.testStream(header, stream, expected);
} else {
await defaultTestStream(header, stream, expected, projectPath);
}
} catch (err) {
reject(err);
reject(err as Error);
}
next();
},
@ -144,9 +143,8 @@ export async function expectStreamNoCRLF(
_header: tar.Headers,
stream: Readable,
): Promise<void> {
const chai = await import('chai');
const buf = await streamToBuffer(stream);
await chai.expect(buf.includes('\r\n')).to.be.false;
expect(buf.includes('\r\n')).to.be.false;
}
/**
@ -184,7 +182,7 @@ export async function testDockerBuildStream(o: {
o.dockerMock.expectPostBuild({
...o,
checkURI: async (uri: string) => {
checkURI: (uri: string) => {
const url = new URL(uri, 'http://test.net/');
const queryParams = Array.from(url.searchParams.entries());
expect(deepJsonParse(queryParams)).to.have.deep.members(
@ -241,7 +239,7 @@ export async function testPushBuildStream(o: {
o.builderMock.expectPostBuild({
...o,
checkURI: async (uri: string) => {
checkURI: (uri: string) => {
const url = new URL(uri, 'http://test.net/');
const queryParams = Array.from(url.searchParams.entries());
expect(deepJsonParse(queryParams)).to.have.deep.members(

View File

@ -287,15 +287,14 @@ export function monochrome(text: string): string {
*/
export function fillTemplate(
templateString: string,
templateVars: object,
templateVars: Record<string, unknown>,
): string {
const escaped = templateString.replace(/\\/g, '\\\\').replace(/`/g, '\\`');
const resolved = new Function(
...Object.keys(templateVars),
`return \`${escaped}\`;`,
).call(null, ...Object.values(templateVars));
const unescaped = resolved.replace(/\\`/g, '`').replace(/\\\\/g, '\\');
return unescaped;
return templateString.replace(/\$\{(\w+)\}/g, (_, key) => {
if (key in templateVars) {
return String(templateVars[key]);
}
throw new Error(`Missing template variable: ${key}`);
});
}
/**

View File

@ -61,22 +61,25 @@ export class BalenaAPIMock extends NockMock {
}
public expectDownloadConfig(opts: ScopeOpts = {}) {
this.optPost('/download-config', opts).reply(
200,
JSON.parse(`{
"applicationId":1301645,
"deviceType":"raspberrypi3",
"userId":43699,
"appUpdatePollInterval":600000,
"listenPort":48484,
"vpnPort":443,
"apiEndpoint":"https://api.balena-cloud.com",
"vpnEndpoint":"vpn.balena-cloud.com",
"registryEndpoint":"registry2.balena-cloud.com",
"deltaEndpoint":"https://delta.balena-cloud.com",
"apiKey":"nothingtoseehere"
}`),
);
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(`{
"applicationId":1301645,
"deviceType":"${deviceType}",
"userId":43699,
"appUpdatePollInterval":600000,
"listenPort":48484,
"vpnPort":443,
"apiEndpoint":"https://api.balena-cloud.com",
"vpnEndpoint":"vpn.balena-cloud.com",
"registryEndpoint":"registry2.balena-cloud.com",
"deltaEndpoint":"https://delta.balena-cloud.com",
"apiKey":"nothingtoseehere"
}`);
});
}
public expectApplicationProvisioning(opts: ScopeOpts = {}) {
@ -283,7 +286,7 @@ export class BalenaAPIMock extends NockMock {
this.optGet(/^\/v\d+\/service_environment_variable($|\?)/, opts).reply(
function (uri, _requestBody) {
const match = uri.match(/service_name%20eq%20%27(.+?)%27/);
const serviceName = (match && match[1]) || undefined;
const serviceName = match?.[1] || undefined;
let varArray: any[];
if (serviceName) {
const varObj = appServiceVarsByService[serviceName];
@ -331,7 +334,7 @@ export class BalenaAPIMock extends NockMock {
opts,
).reply(function (uri, _requestBody) {
const match = uri.match(/service_name%20eq%20%27(.+?)%27/);
const serviceName = (match && match[1]) || undefined;
const serviceName = match?.[1] || undefined;
let varArray: any[];
if (serviceName) {
const varObj = deviceServiceVarsByService[serviceName];

View File

@ -37,7 +37,7 @@ export class BuilderMock extends NockMock {
persist?: boolean;
responseBody: any;
responseCode: number;
checkURI: (uri: string) => Promise<void>;
checkURI: (uri: string) => Promise<void> | void;
checkBuildRequestBody: (requestBody: string | Buffer) => Promise<void>;
}) {
this.optPost(/^\/v3\/build($|[(?])/, opts).reply(

View File

@ -75,7 +75,7 @@ export class DockerMock extends NockMock {
responseBody: any;
responseCode: number;
tag: string;
checkURI: (uri: string) => Promise<void>;
checkURI: (uri: string) => Promise<void> | void;
checkBuildRequestBody: (requestBody: string) => Promise<void>;
}) {
this.optPost(

View File

@ -37,7 +37,7 @@ export class NockMock {
constructor(
public basePathPattern: string | RegExp,
public allowUnmocked: boolean = false,
public allowUnmocked = false,
) {
if (NockMock.instanceCount === 0) {
if (!nock.isActive()) {

View File

@ -50,7 +50,7 @@ export async function exists(fPath: string) {
try {
await fs.stat(fPath);
return true;
} catch (e) {
} catch {
return false;
}
}

View File

@ -10,7 +10,7 @@
"build_log": null,
"start_timestamp": "2021-08-25T22:18:33.624Z",
"end_timestamp": "2021-08-25T22:18:48.820Z",
"contains__image": [
"release_image": [
{
"image": [
{

View File

@ -75,12 +75,12 @@ describe('detectEncoding() function', function () {
for (const fname of sampleBinary) {
const buf = await fs.readFile(path.join('node_modules', fname));
const encoding = await detectEncoding(buf);
const encoding = detectEncoding(buf);
expect(encoding).to.equal('binary');
}
for (const fname of sampleText) {
const buf = await fs.readFile(fname);
const encoding = await detectEncoding(buf);
const encoding = detectEncoding(buf);
expect(encoding).to.equal('utf-8');
}
});

View File

@ -1,5 +1,5 @@
import * as stream from 'stream';
import { AssertionError, expect } from 'chai';
import { expect } from 'chai';
import { stub } from 'sinon';
import * as tmp from 'tmp';
import { delay } from '../../utils';
@ -32,9 +32,7 @@ describe('image-manager', function () {
fs.writeSync(this.image.fd, 'Cache image', 0, 'utf8');
this.cacheGetImagePathStub = stub(imageManager, 'getImagePath');
return this.cacheGetImagePathStub.returns(
Promise.resolve(this.image.name),
);
return this.cacheGetImagePathStub.resolves(this.image.name);
});
afterEach(function () {
@ -44,8 +42,8 @@ describe('image-manager', function () {
describe('given the image is fresh', function () {
beforeEach(function () {
this.cacheIsImageFresh = stub(imageManager, 'isImageFresh');
return this.cacheIsImageFresh.returns(Promise.resolve(true));
this.cacheIsImageFresh = stub(imageManager, 'isImageCached');
return this.cacheIsImageFresh.resolves(true);
});
afterEach(function () {
@ -58,11 +56,11 @@ describe('image-manager', function () {
void imageManager.getStream('raspberry-pi').then(function (stream) {
let result = '';
stream.on('data', (chunk) => (result += chunk.toString()));
stream.on('data', (chunk: string) => (result += chunk.toString()));
return stream.on('end', function () {
expect(result).to.equal('Cache image');
return done();
done();
});
});
});
@ -70,8 +68,8 @@ describe('image-manager', function () {
describe('given the image is not fresh', function () {
beforeEach(function () {
this.cacheIsImageFresh = stub(imageManager, 'isImageFresh');
return this.cacheIsImageFresh.returns(Promise.resolve(false));
this.cacheIsImageFresh = stub(imageManager, 'isImageCached');
return this.cacheIsImageFresh.resolves(false);
});
afterEach(function () {
@ -82,9 +80,7 @@ describe('image-manager', function () {
describe.skip('given a valid download endpoint', function () {
beforeEach(function () {
this.osDownloadStub = stub(balena.models.os, 'download');
this.osDownloadStub.returns(
Promise.resolve(stringToStream('Download image')),
);
this.osDownloadStub.resolves(stringToStream('Download image'));
});
afterEach(function () {
@ -95,7 +91,7 @@ describe('image-manager', function () {
void imageManager.getStream('raspberry-pi').then((stream) => {
let result = '';
stream.on('data', (chunk) => (result += chunk));
stream.on('data', (chunk: string) => (result += chunk));
stream.on('end', async () => {
expect(result).to.equal('Download image');
@ -131,7 +127,7 @@ describe('image-manager', function () {
beforeEach(function () {
this.osDownloadStream = new stream.PassThrough();
this.osDownloadStub = stub(balena.models.os, 'download');
this.osDownloadStub.returns(Promise.resolve(this.osDownloadStream));
this.osDownloadStub.resolves(this.osDownloadStream);
});
afterEach(function () {
@ -153,9 +149,7 @@ describe('image-manager', function () {
const contents = await fsAsync
.stat(this.image.name + '.inprogress')
.then(function () {
throw new AssertionError(
'Image cache should be deleted on failure',
);
throw new Error('Image cache should be deleted on failure');
})
.catch((err) => {
if (err.code !== 'ENOENT') {
@ -174,7 +168,7 @@ describe('image-manager', function () {
});
});
describe('given a stream with the mime property', async function () {
describe('given a stream with the mime property', function () {
beforeEach(function () {
this.osDownloadStub = stub(balena.models.os, 'download');
const message = 'Lorem ipsum dolor sit amet';
@ -184,7 +178,7 @@ describe('image-manager', function () {
mime?: string;
};
mockResultStream.mime = 'application/zip';
this.osDownloadStub.returns(Promise.resolve(mockResultStream));
this.osDownloadStub.resolves(mockResultStream);
});
afterEach(function () {
@ -209,12 +203,10 @@ describe('image-manager', function () {
this.balenaSettingsGetStub
.withArgs('cacheDirectory')
.returns(
Promise.resolve(
os.platform() === 'win32'
? 'C:\\Users\\johndoe\\_balena\\cache'
: '/Users/johndoe/.balena/cache',
),
.resolves(
os.platform() === 'win32'
? 'C:\\Users\\johndoe\\_balena\\cache'
: '/Users/johndoe/.balena/cache',
);
});
@ -228,21 +220,21 @@ describe('image-manager', function () {
balena.models.config,
'getDeviceTypeManifestBySlug',
);
this.getDeviceTypeManifestBySlugStub.withArgs('raspberry-pi').returns(
Promise.resolve({
this.getDeviceTypeManifestBySlugStub
.withArgs('raspberry-pi')
.resolves({
yocto: {
fstype: 'resin-sdcard',
},
}),
);
});
this.getDeviceTypeManifestBySlugStub.withArgs('intel-edison').returns(
Promise.resolve({
this.getDeviceTypeManifestBySlugStub
.withArgs('intel-edison')
.resolves({
yocto: {
fstype: 'zip',
},
}),
);
});
});
afterEach(function () {
@ -288,20 +280,18 @@ describe('image-manager', function () {
});
});
describe('.isImageFresh()', () => {
describe('.isImageCached()', () => {
describe('given the raspberry-pi manifest', function () {
beforeEach(function () {
this.getDeviceTypeManifestBySlugStub = stub(
balena.models.config,
'getDeviceTypeManifestBySlug',
);
this.getDeviceTypeManifestBySlugStub.returns(
Promise.resolve({
yocto: {
fstype: 'balena-sdcard',
},
}),
);
this.getDeviceTypeManifestBySlugStub.resolves({
yocto: {
fstype: 'balena-sdcard',
},
});
});
afterEach(function () {
@ -314,8 +304,8 @@ describe('image-manager', function () {
imageManager,
'getFileCreatedDate',
);
this.utilsGetFileCreatedDate.returns(
Promise.reject(new Error("ENOENT, stat 'raspberry-pi'")),
this.utilsGetFileCreatedDate.rejects(
new Error("ENOENT, stat 'raspberry-pi'"),
);
});
@ -324,78 +314,8 @@ describe('image-manager', function () {
});
it('should return false', async function () {
expect(await imageManager.isImageFresh('raspberry-pi', '1.2.3')).to.be
.false;
});
});
describe('given a fixed created time', function () {
beforeEach(function () {
this.utilsGetFileCreatedDate = stub(
imageManager,
'getFileCreatedDate',
);
this.utilsGetFileCreatedDate.returns(
Promise.resolve(new Date('2014-01-01T00:00:00.000Z')),
);
});
afterEach(function () {
this.utilsGetFileCreatedDate.restore();
});
describe('given the file was created before the os last modified time', function () {
beforeEach(function () {
this.osGetLastModified = stub(balena.models.os, 'getLastModified');
this.osGetLastModified.returns(
Promise.resolve(new Date('2014-02-01T00:00:00.000Z')),
);
});
afterEach(function () {
this.osGetLastModified.restore();
});
it('should return false', function () {
const promise = imageManager.isImageFresh('raspberry-pi', '1.2.3');
return expect(promise).to.eventually.be.false;
});
});
describe('given the file was created after the os last modified time', function () {
beforeEach(function () {
this.osGetLastModified = stub(balena.models.os, 'getLastModified');
this.osGetLastModified.returns(
Promise.resolve(new Date('2013-01-01T00:00:00.000Z')),
);
});
afterEach(function () {
this.osGetLastModified.restore();
});
it('should return true', function () {
const promise = imageManager.isImageFresh('raspberry-pi', '1.2.3');
return expect(promise).to.eventually.be.true;
});
});
describe('given the file was created just at the os last modified time', function () {
beforeEach(function () {
this.osGetLastModified = stub(balena.models.os, 'getLastModified');
this.osGetLastModified.returns(
Promise.resolve(new Date('2014-00-01T00:00:00.000Z')),
);
});
afterEach(function () {
this.osGetLastModified.restore();
});
it('should return false', function () {
const promise = imageManager.isImageFresh('raspberry-pi', '1.2.3');
return expect(promise).to.eventually.be.false;
});
expect(await imageManager.isImageCached('raspberry-pi', '1.2.3')).to
.be.false;
});
});
});
@ -408,7 +328,7 @@ describe('image-manager', function () {
fs.writeSync(this.image.fd, 'Lorem ipsum dolor sit amet', 0, 'utf8');
this.cacheGetImagePathStub = stub(imageManager, 'getImagePath');
this.cacheGetImagePathStub.returns(Promise.resolve(this.image.name));
this.cacheGetImagePathStub.resolves(this.image.name);
});
afterEach(function (done) {
@ -422,7 +342,7 @@ describe('image-manager', function () {
.then(function (stream) {
let result = '';
stream.on('data', (chunk) => (result += chunk));
stream.on('data', (chunk) => (result += chunk as string));
stream.on('end', function () {
expect(result).to.equal('Lorem ipsum dolor sit amet');
@ -445,7 +365,7 @@ describe('image-manager', function () {
beforeEach(function () {
this.image = tmp.fileSync();
this.cacheGetImagePathStub = stub(imageManager, 'getImagePath');
this.cacheGetImagePathStub.returns(Promise.resolve(this.image.name));
this.cacheGetImagePathStub.resolves(this.image.name);
});
afterEach(function (done) {
@ -488,9 +408,7 @@ describe('image-manager', function () {
beforeEach(function () {
this.date = new Date(2014, 1, 1);
this.fsStatStub = stub(fs.promises, 'stat');
this.fsStatStub
.withArgs('foo')
.returns(Promise.resolve({ ctime: this.date }));
this.fsStatStub.withArgs('foo').resolves({ ctime: this.date });
});
afterEach(function () {
@ -508,7 +426,7 @@ describe('image-manager', function () {
this.fsStatStub = stub(fs.promises, 'stat');
this.fsStatStub
.withArgs('foo')
.returns(Promise.reject(new Error("ENOENT, stat 'foo'")));
.rejects(new Error("ENOENT, stat 'foo'"));
});
afterEach(function () {
@ -554,7 +472,9 @@ describe('image-manager', function () {
mockFs({});
});
afterEach(() => mockFs.restore());
afterEach(() => {
mockFs.restore();
});
it('should keep the cache directory removed', async function () {
const exists = await fsExistsAsync(this.cacheDirectory);

View File

@ -117,7 +117,7 @@ describe('disambiguateReleaseParam() function', () => {
it('should return id from SDK on first call, if match is found', async () => {
const input = '1234';
const output = 1234;
const getRelease = sinon.stub().returns(Promise.resolve({ id: output }));
const getRelease = sinon.stub().resolves({ id: output });
const sdk: any = {
models: {
release: {
@ -139,9 +139,9 @@ describe('disambiguateReleaseParam() function', () => {
const getRelease = sinon
.stub()
.onCall(0)
.returns(Promise.reject(new BalenaReleaseNotFound(input)))
.rejects(new BalenaReleaseNotFound(input))
.onCall(1)
.returns(Promise.resolve({ id: output }));
.resolves({ id: output });
const sdk: any = {
models: {
@ -161,9 +161,7 @@ describe('disambiguateReleaseParam() function', () => {
it('should throw error if no match found', async () => {
const input = '1234';
const getRelease = sinon
.stub()
.returns(Promise.reject(new BalenaReleaseNotFound(input)));
const getRelease = sinon.stub().rejects(new BalenaReleaseNotFound(input));
const sdk: any = {
models: {
@ -185,9 +183,7 @@ describe('disambiguateReleaseParam() function', () => {
it('should throw error if unknown error returned from SDK', async () => {
const input = '1234';
const getRelease = sinon
.stub()
.returns(Promise.reject(new Error('some error')));
const getRelease = sinon.stub().rejects(new Error('some error'));
const sdk: any = {
models: {

View File

@ -15,10 +15,14 @@
* limitations under the License.
*/
import { expect } from 'chai';
import * as chaiAsPromised from 'chai-as-promised';
import * as chai from 'chai';
import { ExpectedError } from '../../build/errors';
import * as v from '../../build/utils/validation';
chai.use(chaiAsPromised);
const { expect } = chai;
describe('validateEmail() function', () => {
it('should reject invalid email addresses with a message', () => {
const errorMessage = 'Email is not valid';
@ -186,55 +190,55 @@ describe('validateUuid() function', () => {
describe('parseAsInteger() function', () => {
it('should reject non-numeric characters', () => {
expect(() => v.parseAsInteger('abc')).to.throw(ExpectedError);
expect(() => v.parseAsInteger('1a')).to.throw(ExpectedError);
expect(() => v.parseAsInteger('a1')).to.throw(ExpectedError);
expect(() => v.parseAsInteger('a')).to.throw(ExpectedError);
expect(() => v.parseAsInteger('1.0')).to.throw(ExpectedError);
expect(v.parseAsInteger('abc')).to.be.rejectedWith(ExpectedError);
expect(v.parseAsInteger('1a')).to.be.rejectedWith(ExpectedError);
expect(v.parseAsInteger('a1')).to.be.rejectedWith(ExpectedError);
expect(v.parseAsInteger('a')).to.be.rejectedWith(ExpectedError);
expect(v.parseAsInteger('1.0')).to.be.rejectedWith(ExpectedError);
});
it('should reject leading zeros', () => {
expect(() => v.parseAsInteger('01')).to.throw(ExpectedError);
expect(() => v.parseAsInteger('001')).to.throw(ExpectedError);
expect(v.parseAsInteger('01')).to.be.rejectedWith(ExpectedError);
expect(v.parseAsInteger('001')).to.be.rejectedWith(ExpectedError);
});
it('should throw with specific message when param name passed', () => {
expect(() => v.parseAsInteger('abc')).to.throw(
expect(v.parseAsInteger('abc')).to.be.rejectedWith(
'The parameter must be an integer.',
);
});
it('should throw with general message when no param name passed', () => {
expect(() => v.parseAsInteger('abc', 'foo')).to.throw(
expect(v.parseAsInteger('abc', 'foo')).to.be.rejectedWith(
"The parameter 'foo' must be an integer.",
);
});
it('should parse integers to number type', () => {
expect(v.parseAsInteger('100')).to.equal(100);
expect(v.parseAsInteger('100')).to.be.a('number');
expect(v.parseAsInteger('0')).to.equal(0);
expect(v.parseAsInteger('0')).to.be.a('number');
it('should parse integers to number type', async () => {
expect(await v.parseAsInteger('100')).to.equal(100);
expect(await v.parseAsInteger('100')).to.be.a('number');
expect(await v.parseAsInteger('0')).to.equal(0);
expect(await v.parseAsInteger('0')).to.be.a('number');
});
});
describe('tryAsInteger() function', () => {
it('should return string with non-numeric characters as string', () => {
expect(v.tryAsInteger('abc')).to.be.a('string');
expect(v.tryAsInteger('1a')).to.be.a('string');
expect(v.tryAsInteger('a1')).to.be.a('string');
expect(v.tryAsInteger('a')).to.be.a('string');
expect(v.tryAsInteger('1.0')).to.be.a('string');
it('should return string with non-numeric characters as string', async () => {
expect(await v.tryAsInteger('abc')).to.be.a('string');
expect(await v.tryAsInteger('1a')).to.be.a('string');
expect(await v.tryAsInteger('a1')).to.be.a('string');
expect(await v.tryAsInteger('a')).to.be.a('string');
expect(await v.tryAsInteger('1.0')).to.be.a('string');
});
it('should return numerical strings with leading zeros as string', () => {
expect(v.tryAsInteger('01')).to.be.a('string');
expect(v.tryAsInteger('001')).to.be.a('string');
it('should return numerical strings with leading zeros as string', async () => {
expect(await v.tryAsInteger('01')).to.be.a('string');
expect(await v.tryAsInteger('001')).to.be.a('string');
});
it('should return numerical strings without leading zeros as number', () => {
expect(v.tryAsInteger('100')).to.be.a('number');
expect(v.tryAsInteger('256')).to.be.a('number');
expect(v.tryAsInteger('0')).to.be.a('number');
it('should return numerical strings without leading zeros as number', async () => {
expect(await v.tryAsInteger('100')).to.be.a('number');
expect(await v.tryAsInteger('256')).to.be.a('number');
expect(await v.tryAsInteger('0')).to.be.a('number');
});
});

View File

@ -9,6 +9,7 @@
"./src/**/*",
"./patches/*",
"./tests/**/*",
"./typings/**/*"
"./typings/**/*",
".mocharc-standalone.js",
]
}

View File

@ -32,7 +32,7 @@ declare module 'JSONStream' {
recurse: boolean;
}
export function parse(pattern: any | any[]): NodeJS.ReadWriteStream;
export function parse(pattern: any): NodeJS.ReadWriteStream;
type NewlineOnlyIndicator = false;

View File

@ -1,105 +0,0 @@
/**
* @license
* Copyright 2019 Balena Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
declare module 'balena-device-init' {
import { DeviceTypeJson } from 'balena-sdk';
interface OperationState {
operation:
| CopyOperation
| ReplaceOperation
| RunScriptOperation
| BurnOperation;
percentage: number;
}
interface Operation {
command: string;
}
interface CopyOperation extends Operation {
command: 'copy';
from: { path: string };
to: { path: string };
}
interface ReplaceOperation extends Operation {
command: 'replace';
copy: string;
replace: string;
file: {
path: string;
};
}
interface RunScriptOperation extends Operation {
command: 'run-script';
script: string;
arguments?: string[];
}
interface BurnOperation extends Operation {
command: 'burn';
image?: string;
}
interface BurnProgress {
type: 'write' | 'check';
percentage: number;
transferred: number;
length: number;
remaining: number;
eta: number;
runtime: number;
delta: number;
speed: number;
}
interface InitializeEmitter {
on(event: 'stdout' | 'stderr', callback: (msg: string) => void): void;
on(event: 'state', callback: (state: OperationState) => void): void;
on(event: 'burn', callback: (state: BurnProgress) => void): void;
on(event: 'end', callback: () => void): void;
on(event: 'error', callback: (error: Error) => void): void;
}
// As of writing this, these are Bluebird promises, but we are typing then
// as normal Promises so that we do not rely on Bluebird specific methods,
// so that the CLI will not require any change once the package drops Bluebird.
export function configure(
image: string,
manifest: BalenaSdk.DeviceTypeJson.DeviceType.DeviceType,
config: object,
options?: object,
): Promise<InitializeEmitter>;
export function initialize(
image: string,
manifest: BalenaSdk.DeviceTypeJson.DeviceType.DeviceType,
config: object,
): Promise<InitializeEmitter>;
export function getImageOsVersion(
image: string,
manifest: BalenaSdk.DeviceTypeJson.DeviceType.DeviceType,
): Promise<string | null>;
export function getImageManifest(
image: string,
): Promise<BalenaSdk.DeviceTypeJson.DeviceType.DeviceType | null>;
}