Compare commits

...

92 Commits

Author SHA1 Message Date
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
36077cacda v20.1.0 2024-12-12 14:17:22 +00:00
82b9983450 Merge pull request #2890 from balena-io/takeover
`device os-update`: Add support for updates that require takeover
2024-12-12 09:16:14 -05:00
703dbd01c9 device os-update: Add handling for updates that require takeover
Change-type: minor
2024-12-11 12:04:53 -05:00
602e63c8a9 Fix typings in release and release list commands 2024-12-11 12:00:08 -05:00
2ab635f49a Update expected warnings 2024-12-11 12:00:08 -05:00
322736a145 Deduplicate dependencies 2024-12-11 12:00:08 -05:00
c347b67b25 Update balena-sdk
Change-type: patch
2024-12-11 12:00:08 -05:00
4022beeb56 Update @balena/compose
Change-type: patch
2024-12-11 12:00:08 -05:00
ccf97cfc9f v20.0.9 2024-12-05 12:08:28 +00:00
9c5fe14f2e Merge pull request #2889 from oskarwilliams/express-version-4.21.1
Update shrinkwrapped express to v4.21.1
2024-12-05 12:07:23 +00:00
38e29251e7 Update shrinkwrapped express to v4.21.1
Change-type: patch
2024-12-05 09:59:57 +00:00
bfc7a14646 v20.0.8 2024-12-04 19:37:38 +00:00
610db81fcb Merge pull request #2893 from balena-io/bump-macos-13
Run test and publish with macos-13
2024-12-04 16:36:42 -03:00
d1f7d6d07f Run test and publish with macos-13
Change-type: patch
2024-12-04 16:16:52 -03:00
694eb78aaa v20.0.7 2024-11-23 17:38:10 +00:00
1caccafbcd Merge pull request #2891 from balena-io/bump-typescript-to-5.7.2
Bump typescript to 5.7.2
2024-11-23 17:37:10 +00:00
61d4d1f1e7 Update TypeScript to 5.7.2
Change-type: patch
2024-11-23 00:40:43 +02:00
a01c85bc15 Deduplicate dependencies 2024-11-23 00:40:28 +02:00
5d7b7cfc6f v20.0.6 2024-11-08 08:10:12 +00:00
92fd9e0883 Merge pull request #2887 from balena-io/refactor-build
Refactor balena build for clarity
2024-11-08 08:09:19 +00:00
24273b5ac0 Deduplicate dependencies 2024-11-07 17:33:14 +02:00
6155509f4c Refactor balena build for clarity
Change-type: patch
2024-11-07 16:24:10 +02:00
99 changed files with 5910 additions and 2518 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@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # 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@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # 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

@ -26,7 +26,7 @@ jobs:
"os": [
["self-hosted", "X64"],
["self-hosted", "ARM64"],
["macos-12"],
["macos-13"],
["windows-2019"],
["macos-latest-xlarge"]
]
@ -36,7 +36,7 @@ jobs:
"os": [
["self-hosted", "X64"],
["self-hosted", "ARM64"],
["macos-12"],
["macos-13"],
["windows-2019"],
["macos-latest-xlarge"]
]

File diff suppressed because it is too large Load Diff

View File

@ -4,6 +4,236 @@ 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.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]
* Update `balena-sdk` [myarmolinsky]
* Update `@balena/compose` [myarmolinsky]
## 20.0.9 - 2024-12-05
* Update shrinkwrapped express to v4.21.1 [Oskar Williams]
## 20.0.8 - 2024-12-04
* Run test and publish with macos-13 [Otavio Jacobi]
## 20.0.7 - 2024-11-23
* Update TypeScript to 5.7.2 [Thodoris Greasidis]
## 20.0.6 - 2024-11-08
* Refactor balena build for clarity [Thodoris Greasidis]
## 20.0.5 - 2024-11-05
* Update actions/upload-artifact digest to b4b15b8 [balena-renovate[bot]]

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: '^_',
}],
},
}),
];

5729
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.0.5",
"version": "21.1.0",
"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.6.2"
"typescript": "^5.8.2"
},
"dependencies": {
"@balena/compose": "^5.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.3.0",
"balena-image-fs": "^7.3.0",
"balena-preload": "^18.0.1",
"balena-sdk": "^21.2.1",
"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.5",
"docker-progress": "^5.1.3",
"dockerode": "^4.0.2",
"dockerode": "4.0.3",
"ejs": "^3.1.6",
"etcher-sdk": "9.1.0",
"express": "^4.17.2",
@ -274,6 +276,6 @@
}
},
"versionist": {
"publishedAt": "2024-11-05T10:12:23.858Z"
"publishedAt": "2025-03-12T19:34:18.617Z"
}
}

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

@ -36,14 +36,16 @@ import { buildProject, composeCliFlags } from '../../utils/compose_ts';
import type { BuildOpts, DockerCliFlags } from '../../utils/docker';
import { dockerCliFlags } from '../../utils/docker';
// TODO: For this special one we can't use Interfaces.InferredFlags/InferredArgs
// because of the 'registry-secrets' type which is defined in the actual code
// as a path (string | undefined) but then the cli turns it into an object
interface FlagsDef extends ComposeCliFlags, DockerCliFlags {
type ComposeGenerateOptsParam = Parameters<typeof compose.generateOpts>[0];
interface PrepareBuildOpts
extends ComposeCliFlags,
DockerCliFlags,
ComposeGenerateOptsParam {
arch?: string;
deviceType?: string;
fleet?: string;
source?: string; // Not part of command profile - source param copied here.
source?: string;
}
export default class BuildCmd extends Command {
@ -113,29 +115,31 @@ ${dockerignoreHelp}
const logger = Logger.getLogger();
logger.logDebug('Parsing input...');
// `build` accepts `source` as a parameter, but compose expects it as an option
options.source = params.source;
delete params.source;
const prepareBuildOpts = {
...options,
source: params.source,
};
await this.resolveArchFromDeviceType(sdk, options);
await this.resolveArchFromDeviceType(sdk, prepareBuildOpts);
await this.validateOptions(options, sdk);
await this.validateOptions(prepareBuildOpts, sdk);
// Build args are under consideration for removal - warn user
if (options.buildArg) {
if (prepareBuildOpts.buildArg) {
console.log(buildArgDeprecation);
}
const app = await this.getAppAndResolveArch(options);
const app = await this.getAppAndResolveArch(prepareBuildOpts);
const { docker, buildOpts, composeOpts } = await this.prepareBuild(options);
const { docker, buildOpts, composeOpts } =
await this.prepareBuild(prepareBuildOpts);
try {
await this.buildProject(docker, logger, composeOpts, {
appType: app?.application_type?.[0],
arch: options.arch!,
deviceType: options.deviceType!,
buildEmulated: options.emulated,
arch: prepareBuildOpts.arch!,
deviceType: prepareBuildOpts.deviceType!,
buildEmulated: prepareBuildOpts.emulated,
buildOpts,
});
} catch (err) {
@ -147,7 +151,7 @@ ${dockerignoreHelp}
logger.logSuccess('Build succeeded!');
}
protected async validateOptions(opts: FlagsDef, sdk: BalenaSDK) {
protected async validateOptions(opts: PrepareBuildOpts, sdk: BalenaSDK) {
// Validate option combinations
if (
(opts.fleet == null && (opts.arch == null || opts.deviceType == null)) ||
@ -175,7 +179,10 @@ ${dockerignoreHelp}
opts['registry-secrets'] = registrySecrets;
}
protected async resolveArchFromDeviceType(sdk: BalenaSDK, opts: FlagsDef) {
protected async resolveArchFromDeviceType(
sdk: BalenaSDK,
opts: PrepareBuildOpts,
) {
if (opts.deviceType != null && opts.arch == null) {
try {
const deviceTypeOpts = {
@ -208,7 +215,7 @@ ${dockerignoreHelp}
}
}
protected async getAppAndResolveArch(opts: FlagsDef) {
protected async getAppAndResolveArch(opts: PrepareBuildOpts) {
if (opts.fleet) {
const { getAppWithArch } = await import('../../utils/helpers');
const app = await getAppWithArch(opts.fleet);
@ -218,7 +225,7 @@ ${dockerignoreHelp}
}
}
protected async prepareBuild(options: FlagsDef) {
protected async prepareBuild(options: PrepareBuildOpts) {
const { getDocker, generateBuildOpts } = await import('../../utils/docker');
const [docker, buildOpts, composeOpts] = await Promise.all([
getDocker(options),

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

@ -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

@ -20,6 +20,7 @@ import * as cf from '../../utils/common-flags';
import { getBalenaSdk, stripIndent, getCliForm } from '../../utils/lazy';
import type { Device } from 'balena-sdk';
import { ExpectedError } from '../../errors';
import { getExpandedProp } from '../../utils/pine';
export default class DeviceOsUpdateCmd extends Command {
public static description = stripIndent`
@ -126,20 +127,46 @@ export default class DeviceOsUpdateCmd extends Command {
);
}
} else {
const choices = await Promise.all(
hupVersionInfo.versions.map(async (version) => {
const takeoverRequired =
(await sdk.models.os.getOsUpdateType(
getExpandedProp(is_of__device_type, 'slug')!,
currentOsVersion,
version,
)) === 'takeover';
return {
name: `${version}${hupVersionInfo.recommended === version ? ' (recommended)' : ''}${takeoverRequired ? ' ADVANCED UPDATE: Requires disk re-partitioning with no rollback option' : ''}`,
value: version,
};
}),
);
targetOsVersion = await getCliForm().ask({
message: 'Target OS version',
type: 'list',
choices: hupVersionInfo.versions.map((version) => ({
name:
hupVersionInfo.recommended === version
? `${version} (recommended)`
: version,
value: version,
})),
choices,
});
}
const takeoverRequired =
(await sdk.models.os.getOsUpdateType(
getExpandedProp(is_of__device_type, 'slug')!,
currentOsVersion,
targetOsVersion,
)) === 'takeover';
const patterns = await import('../../utils/patterns');
// Warn the user if the update requires a takeover
if (takeoverRequired) {
await patterns.confirm(
options.yes || false,
stripIndent`Before you proceed, note that this update process is different from a regular HostOS Update:
DATA LOSS: This update requires disk re-partitioning, which will erase all data stored on the device.
NO ROLLBACK: Unlike our HostOS update mechanism, this process does not allow reverting to a previous version in case of failure.
Make sure to back up all important data before continuing. For more details, check our documentation: https://docs.balena.io/reference/OS/updates/update-process/
`,
);
}
// Confirm and start update
await patterns.confirm(
options.yes || false,

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

@ -109,7 +109,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 +126,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,7 +155,7 @@ Can be repeated to add multiple certificates.\
------------------------------------------------------------------------------
`);
}
} catch (error) {
} catch {
throw new ExpectedError(
`The provided image path does not exist: ${params.image}`,
);
@ -192,11 +192,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 +295,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>;
@ -86,7 +86,7 @@ export default class ReleaseCmd extends Command {
balena: BalenaSdk.BalenaSDK,
options: FlagsDef,
) {
const fields: Array<keyof BalenaSdk.Release> = [
const fields = [
'id',
'commit',
'created_at',
@ -96,7 +96,7 @@ export default class ReleaseCmd extends Command {
'build_log',
'start_timestamp',
'end_timestamp',
];
] satisfies BalenaSdk.PineOptions<BalenaSdk.Release>['$select'];
const release = await balena.models.release.get(commitOrId, {
...(!options.json && { $select: fields }),

View File

@ -56,14 +56,14 @@ export default class ReleaseListCmd extends Command {
public async run() {
const { args: params, flags: options } = await this.parse(ReleaseListCmd);
const fields: Array<keyof BalenaSdk.Release> = [
const fields = [
'id',
'commit',
'created_at',
'status',
'semver',
'is_final',
];
] satisfies BalenaSdk.PineOptions<BalenaSdk.Release>['$select'];
const balena = getBalenaSdk();
const { getFleetSlug } = await import('../../utils/sdk');

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

@ -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

@ -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

@ -373,6 +373,8 @@
> Warning Failed to make bytecode node20-arm64 for file node_modules/axios/lib/platform/node/index.js
> Warning Failed to make bytecode node20-arm64 for file node_modules/axios/lib/platform/node/classes/FormData.js
> Warning Failed to make bytecode node20-arm64 for file node_modules/axios/lib/platform/node/classes/URLSearchParams.js
> Warning Failed to make bytecode node20-arm64 for file node_modules/form-data-encoder/lib/index.js
> Warning Failed to make bytecode node20-arm64 for file node_modules/formdata-node/lib/form-data.js
> Warning Failed to make bytecode node20-arm64 for file node_modules/@isaacs/cliui/node_modules/string-width/index.js
> Warning Failed to make bytecode node20-arm64 for file node_modules/@isaacs/cliui/node_modules/strip-ansi/index.js
> Warning Failed to make bytecode node20-arm64 for file node_modules/@isaacs/cliui/node_modules/wrap-ansi/index.js

View File

@ -373,6 +373,8 @@
> Warning Failed to make bytecode node20-x64 for file node_modules/axios/lib/platform/node/index.js
> Warning Failed to make bytecode node20-x64 for file node_modules/axios/lib/platform/node/classes/FormData.js
> Warning Failed to make bytecode node20-x64 for file node_modules/axios/lib/platform/node/classes/URLSearchParams.js
> Warning Failed to make bytecode node20-x64 for file node_modules/form-data-encoder/lib/index.js
> Warning Failed to make bytecode node20-x64 for file node_modules/formdata-node/lib/form-data.js
> Warning Failed to make bytecode node20-x64 for file node_modules/@isaacs/cliui/node_modules/string-width/index.js
> Warning Failed to make bytecode node20-x64 for file node_modules/@isaacs/cliui/node_modules/strip-ansi/index.js
> Warning Failed to make bytecode node20-x64 for file node_modules/@isaacs/cliui/node_modules/wrap-ansi/index.js

View File

@ -373,6 +373,8 @@
> Warning Failed to make bytecode node20-arm64 for file node_modules/axios/lib/platform/node/index.js
> Warning Failed to make bytecode node20-arm64 for file node_modules/axios/lib/platform/node/classes/FormData.js
> Warning Failed to make bytecode node20-arm64 for file node_modules/axios/lib/platform/node/classes/URLSearchParams.js
> Warning Failed to make bytecode node20-arm64 for file node_modules/form-data-encoder/lib/index.js
> Warning Failed to make bytecode node20-arm64 for file node_modules/formdata-node/lib/form-data.js
> Warning Failed to make bytecode node20-arm64 for file node_modules/@isaacs/cliui/node_modules/string-width/index.js
> Warning Failed to make bytecode node20-arm64 for file node_modules/@isaacs/cliui/node_modules/strip-ansi/index.js
> Warning Failed to make bytecode node20-arm64 for file node_modules/@isaacs/cliui/node_modules/wrap-ansi/index.js

View File

@ -373,6 +373,8 @@
> Warning Failed to make bytecode node20-x64 for file node_modules/axios/lib/platform/node/index.js
> Warning Failed to make bytecode node20-x64 for file node_modules/axios/lib/platform/node/classes/FormData.js
> Warning Failed to make bytecode node20-x64 for file node_modules/axios/lib/platform/node/classes/URLSearchParams.js
> Warning Failed to make bytecode node20-x64 for file node_modules/form-data-encoder/lib/index.js
> Warning Failed to make bytecode node20-x64 for file node_modules/formdata-node/lib/form-data.js
> Warning Failed to make bytecode node20-x64 for file node_modules/@isaacs/cliui/node_modules/string-width/index.js
> Warning Failed to make bytecode node20-x64 for file node_modules/@isaacs/cliui/node_modules/strip-ansi/index.js
> Warning Failed to make bytecode node20-x64 for file node_modules/@isaacs/cliui/node_modules/wrap-ansi/index.js

View File

@ -373,6 +373,8 @@
> Warning Failed to make bytecode node20-x64 for file node_modules\axios\lib\platform\node\index.js
> Warning Failed to make bytecode node20-x64 for file node_modules\axios\lib\platform\node\classes\FormData.js
> Warning Failed to make bytecode node20-x64 for file node_modules\axios\lib\platform\node\classes\URLSearchParams.js
> Warning Failed to make bytecode node20-x64 for file node_modules\form-data-encoder\lib\index.js
> Warning Failed to make bytecode node20-x64 for file node_modules\formdata-node\lib\form-data.js
> Warning Failed to make bytecode node20-x64 for file node_modules\@isaacs\cliui\node_modules\string-width\index.js
> Warning Failed to make bytecode node20-x64 for file node_modules\@isaacs\cliui\node_modules\strip-ansi\index.js
> Warning Failed to make bytecode node20-x64 for file node_modules\@isaacs\cliui\node_modules\wrap-ansi\index.js

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>;
}