Compare commits

...

90 Commits

Author SHA1 Message Date
9b6fb62e4f Update resin-compose-parse from 2.1.3 to 2.3.0
This adds support for Docker compose v2.4

Closes: #2364
Change-type: patch
Signed-off-by: Tomás Migone <tomas@balena.io>
2021-10-19 16:54:42 -03:00
cc60e86507 v12.50.2 2021-10-05 13:32:02 +03:00
bd774e8553 Merge pull request #2357 from balena-io/fix-fleet-rename-error-message
Error message when renaming a fleet now mentions the target name.
2021-10-05 10:27:51 +00:00
c493c33e38 Error message when renaming a fleet now mentions the target name.
Change-type: patch
Signed-off-by: Carlo Miguel F. Cruz <carloc@balena.io>
2021-10-05 17:01:07 +08:00
9487b33144 v12.50.1 2021-09-30 03:51:16 +03:00
befdae1b90 Merge pull request #2353 from balena-io/fix-help-release-finalize
Fix help output for 'release finalize' command
2021-09-30 00:49:35 +00:00
08dfc945f3 Update dependencies (@sentry/node error reporting)
Change-type: patch
2021-09-30 01:12:17 +01:00
8791c2f4e1 Replace mixpanel dependency with simple GET request
Change-type: patch
2021-09-30 01:12:17 +01:00
be306e6a20 Avoid NockMock warnings during standalone executable testing
Change-type: patch
2021-09-30 01:12:17 +01:00
6cfff72c59 Fix help output for 'release finalize' command
Change-type: patch
2021-09-30 01:12:17 +01:00
adae718c2e v12.50.0 2021-09-28 18:07:49 +03:00
132e1a63b2 Merge pull request #2345 from balena-io/add-release-handling
Add support for releases
2021-09-28 15:05:40 +00:00
a18e182ae4 Add support for releases
Signed-off-by: Paul Jonathan <pj@balena.io>
Change-type: minor
2021-09-28 14:28:43 +00:00
e098cdca17 v12.49.0 2021-09-23 23:56:47 +03:00
b42af74983 Merge pull request #2301 from balena-io/add-multiarch-handling
Add multiarch support
2021-09-23 20:54:51 +00:00
8bb211e441 build, deploy: Improve logging of image build messages
Change-type: patch
2021-09-23 16:37:45 +00:00
ffccbfba12 build, deploy: Add support for multiarch base images
Bump version of balena-multibuild to the one that supports multiarch
Remove previous hack to avoid sending platform information to multibuild

Change-type: minor
Signed-off-by: Paul Jonathan <pj@balena.io>
See: https://github.com/balena-io/balena-cli/issues/1508
2021-09-23 16:37:45 +00:00
56c1af50c0 v12.48.15 2021-09-22 17:52:15 +03:00
8b9e3ccdc8 Merge pull request #2346 from balena-io/update-sdk
Update balena-sdk to 15.51.1
2021-09-22 14:50:21 +00:00
de95262f93 Update balena-sdk to 15.51.1
Update balena-sdk from 15.48.0 to 15.51.1

Change-type: patch
2021-09-22 12:46:34 +05:30
ed49938504 v12.48.14 2021-09-22 00:37:13 +03:00
52ad0f6a57 Merge pull request #2347 from balena-io/klutchell/set-zlib-flush
os download: Avoid incomplete os downloads appearing as successful
2021-09-21 21:35:23 +00:00
7f6738c73c os download: Avoid incomplete os downloads appearing as successful
By forcing the zlib flush mode to Z_NO_FLUSH we are more likely to
see an error on image download pipelines vs silent failure and
incomplete files.

This is part of a larger investigation and may be removed in the
future when the root cause of the pipeline failures are identified.

Change-type: patch
Signed-off-by: Kyle Harding <kyle@balena.io>
2021-09-20 13:28:14 -04:00
88fc3f7714 v12.48.13 2021-09-17 17:01:55 +03:00
1afb29b923 Merge pull request #2340 from balena-io/2339-config-inject-auth
config inject: Remove requirement of being logged in
2021-09-17 13:59:18 +00:00
09a4e8db2d config inject: Remove requirement of being logged in
Change-type: patch
2021-09-16 12:15:53 +01:00
6c81440428 v12.48.12 2021-09-13 18:28:39 +03:00
3eca65ce0d Merge pull request #2333 from balena-io/klutchell/qemu-6-0-0
build/deploy: Update QEMU to 6.0.0 for emulated builds
2021-09-13 15:26:41 +00:00
6319b9dc13 v12.48.11 2021-09-11 01:38:46 +03:00
290acaecbb Merge pull request #2328 from balena-io/825-1018-fix-build-deploy-tag
build, deploy: Fix processing of '--tag' option
2021-09-10 22:36:18 +00:00
305c9045f0 build, deploy: Fix processing of '--tag' option
Change-type: patch
Resolves: #825
Resolves: #1018
2021-09-10 23:11:20 +01:00
b701151769 build/deploy: Update QEMU to 6.0.0 for emulated builds
Change-type: patch
Signed-off-by: Kyle Harding <kyle@balena.io>
2021-09-10 13:32:12 +00:00
e03bbb7275 v12.48.10 2021-09-10 14:25:45 +03:00
3fd66c39ae Merge pull request #2331 from balena-io/2330-retry-supervisor-api-request
push: Await and retry supervisor API requests to a local device
2021-09-10 11:23:59 +00:00
b30075a18b push: Await and retry supervisor API requests to a local device
Change-type: patch
2021-09-10 01:44:26 +01:00
a4fc95e99b v12.48.9 2021-09-10 03:44:17 +03:00
63d8e5e6a3 Merge pull request #2332 from balena-io/bump-net-keepalive-v3.0.0
chore: Update net-keepalive dependency (fix CLI packaging errors)
2021-09-10 00:41:55 +00:00
6244af3464 chore: Update net-keepalive dependency (fix CLI packaging errors)
Change-type: patch
2021-09-10 00:20:26 +01:00
8773927b3f v12.48.8 2021-09-09 00:27:52 +03:00
29a3fd40a2 Merge pull request #2326 from balena-io/v13-gitignore-feature-switch
v13 preparations: Add feature switch for removal of '--gitignore' (push, build)
2021-09-08 21:24:42 +00:00
d6faf060e6 v13 preparations: Add feature switch for removal of '--gitignore' (push, build)
Change-type: patch
2021-09-08 18:10:22 +01:00
352fd197b7 v13 preparations: Adjust test cases for 'balena envs'
Change-type: patch
2021-09-08 17:48:16 +01:00
afb6f938b7 v13 preparations: Adjust test cases for 'balena devices'
Change-type: patch
2021-09-08 17:47:40 +01:00
d3adbcdba9 v12.48.7 2021-09-07 18:28:57 +03:00
33fce1f24f Merge pull request #2325 from balena-io/move-reduce-device-type-json
device move: Rely on the device type model to get the compatible apps
2021-09-07 15:26:20 +00:00
ab90a5f150 v12.48.6 2021-09-07 16:55:22 +03:00
a8b2212fed Merge pull request #2324 from balena-io/reduce-device-type-json
preload: Rely on the device type model to get the compatible apps
2021-09-07 13:53:31 +00:00
6bb8df30dd preload: Rely on the device type model to get the compatible apps
Connects-to: #2318
Change-type: patch
Signed-off-by: Thodoris Greasidis <thodoris@balena.io>
2021-09-07 14:53:17 +03:00
0327ed766d device move: Improve types & reduce the number of API requests
Change-type: patch
Signed-off-by: Thodoris Greasidis <thodoris@balena.io>
2021-09-07 11:43:35 +00:00
1009958340 v12.48.5 2021-09-07 14:12:24 +03:00
5ce17ea70f Merge pull request #2323 from balena-io/no-my_application
preload: Replace my_application query with the SDKs application.getAll()
2021-09-07 11:10:21 +00:00
9c821511b1 device move: Rely on the device type model to get the compatible apps
Connects-to: #2318
Change-type: patch
See: https://www.flowdock.com/app/rulemotion/i-cli/threads/s6x4Z_LoH8IG4PC_YeXMC0TP6v-
Signed-off-by: Thodoris Greasidis <thodoris@balena.io>
2021-09-07 07:53:02 +00:00
d793335287 preload: Replace my_application query with the SDKs application.getAll()
Change-type: patch
Signed-off-by: Thodoris Greasidis <thodoris@balena.io>
2021-09-07 02:09:39 +03:00
dc59b7e4b0 v12.48.4 2021-08-31 04:34:45 +03:00
370b844538 Merge pull request #2316 from balena-io/os-download-no-device-types-v1
os download: Use the hostApps instead of the device-types/v1 endpoint
2021-08-31 01:32:58 +00:00
a8c2724929 v12.48.3 2021-08-31 04:03:14 +03:00
09dd2dd354 Merge pull request #2317 from balena-io/balena-deploy-no-device-types-v1
balena deploy: Retrieve the cpu arch as part of the device type resource
2021-08-31 01:01:30 +00:00
f3ab41841a v12.48.2 2021-08-31 02:23:23 +03:00
3dee30a0fe Merge pull request #2313 from balena-io/install-docs-20210822
Clarify installation instructions
2021-08-30 23:21:40 +00:00
d34073f695 os download: Use the hostApps instead of the device-types/v1 endpoint
Connects-to: #2318
Change-type: patch
Signed-off-by: Thodoris Greasidis <thodoris@balena.io>
2021-08-30 22:32:11 +00:00
24fe6666e4 balena deploy: Retrieve the cpu arch as part of the device type resource
Connects-to: #2318
Change-type: patch
Signed-off-by: Thodoris Greasidis <thodoris@balena.io>
2021-08-30 22:30:16 +00:00
3fd5981085 Clarify installation instructions
Change-type: patch
2021-08-29 02:08:53 +01:00
08ee8643cb v12.48.1 2021-08-27 03:23:23 +03:00
8db36ccec9 Merge pull request #2312 from balena-io/remove-exitWithExpectedError
build, deploy: Extend CTRL-C coverage on Windows (PowerShell, cmd.exe)
2021-08-27 00:21:24 +00:00
deb3e4c4ac Improve error handling (remove most occurrences of process.exit())
Finally delete the deprecated exitWithExpectedError() function from
'lib/errors.ts'.

Change-type: patch
2021-08-27 00:53:21 +01:00
a8ff21af69 build, deploy: Extend CTRL-C coverage on Windows (PowerShell, cmd.exe)
Before this commit, `balena build` and `balena deploy` would almost
never respect CTRL-C on Windows (PowerShell, cmd.exe). Now CTRL-C
is respected over a large extent of runtime and, if CTRL-C is hit
while images are being uploaded (`balena deploy`), the release status
is correctly set to 'failed'.

Change-type: patch
2021-08-27 00:53:21 +01:00
4c54d6c171 v12.48.0 2021-08-26 18:38:44 +03:00
83f213c007 Merge pull request #2274 from balena-io/deploy-release-versioning
Add balena.yml handling to `balena deploy` release creation
2021-08-26 15:36:59 +00:00
d0cdc900a2 Add contract contents at release creation time
Change-type: patch
2021-08-26 16:11:23 +01:00
9937b91606 Documentation update with debugging notes
Signed-off-by: Paul Jonathan <pj@balena.io>
2021-08-26 00:34:54 +00:00
972c2470c5 Fix env variable to avoid test failures
Signed-off-by: Paul Jonathan <pj@balena.io>
Change-type: patch
2021-08-25 17:01:22 +00:00
7d568a928b Add balena.yml handling and --draft to balena deploy release creation
This change allows use of a contract and release semver when doing a push,
and is part of the larger feature to use the builder as part of a CI/CD pipeline.

Change-type: minor
Signed-off-by: Paul Jonathan <pj@balena.io>
2021-08-25 17:01:17 +00:00
2331e0a3e5 v12.47.0 2021-08-20 02:15:13 +03:00
cb9b6be24b Merge pull request #2309 from balena-io/warn-deprecation-policy
Add deprecation policy checker and --unsupported global flag
2021-08-19 23:13:25 +00:00
c2d3eee7cc Add deprecation policy checker and --unsupported global flag
Change-type: minor
2021-08-19 23:17:31 +01:00
d8b08f7272 v12.46.2 2021-08-17 02:27:23 +03:00
819bdac354 Merge pull request #2308 from balena-io/bump-sdk-15.48.0
Update dependencies (balena-sdk from v15.36.0 to v15.48.0)
2021-08-16 23:25:22 +00:00
318de8f017 Update dependencies (balena-sdk from v15.36.0 to v15.48.0)
Update balena-sdk from 15.36.0 to 15.48.0

Change-type: patch
2021-08-16 23:33:44 +01:00
2b0341e12a v12.46.1 2021-08-16 18:08:37 +03:00
21f7463607 Merge pull request #2303 from balena-io/preload-custom-dind
preload: Restore support for armv7 with custom preload image
2021-08-16 15:06:32 +00:00
19fd3094d1 preload: Restore support for armv7 with custom preload image
Update balena-preload from 10.4.20 to 10.5.0

Change-type: patch
Signed-off-by: Kyle Harding <kyle@balena.io>
2021-08-16 09:29:14 -04:00
7c4974f4f5 v12.46.0 2021-08-15 20:09:40 +03:00
3b56ed278e Merge pull request #2270 from balena-io/allow-draft-releases
Add `--draft` option to `balena push`
2021-08-15 17:07:36 +00:00
254ef1c8cf Add --draft option to balena push
This change will allow to build releases as draft and have them being
set as final at a later stage. This change is part of a larger feature towards
using the builder as part of CI/CD pipelines.

Depends-on: https://github.com/balena-io/balena-builder/pull/868
Change-type: minor
2021-08-15 16:43:01 +00:00
d11f49e0f8 v12.45.2 2021-08-14 03:23:04 +03:00
48d7d0ef5e Merge pull request #2305 from balena-io/performResolution-error-handling
push, build: Improve error handling (identify which service failed)
2021-08-14 00:21:11 +00:00
c7bbbc4159 push, build: Improve error handling (identify which service failed)
Change-type: patch
2021-08-13 17:11:48 +01:00
d2fabcaf30 v12.45.1 2021-08-11 17:27:05 +03:00
e137c2aed2 Merge pull request #2307 from balena-io/2306-env-add-app-is-ambiguous
envs, env add: Fix "Application is ambiguous" when using device UUID
2021-08-11 14:25:21 +00:00
58704b08d3 envs, env add: Fix "Application is ambiguous" when using device UUID
Change-type: patch
2021-08-11 02:00:35 +01:00
99 changed files with 4732 additions and 1335 deletions

File diff suppressed because it is too large Load Diff

View File

@ -4,6 +4,282 @@ All notable changes to this project will be documented in this file
automatically by Versionist. DO NOT EDIT THIS FILE MANUALLY! automatically by Versionist. DO NOT EDIT THIS FILE MANUALLY!
This project adheres to [Semantic Versioning](http://semver.org/). This project adheres to [Semantic Versioning](http://semver.org/).
## 12.50.2 - 2021-10-05
* Error message when renaming a fleet now mentions the target name. [Carlo Miguel F. Cruz]
## 12.50.1 - 2021-09-30
* Update dependencies (@sentry/node error reporting) [Paulo Castro]
* Replace mixpanel dependency with simple GET request [Paulo Castro]
* Avoid NockMock warnings during standalone executable testing [Paulo Castro]
* Fix help output for 'release finalize' command [Paulo Castro]
## 12.50.0 - 2021-09-28
* Add support for releases [Paul Jonathan Zoulin]
## 12.49.0 - 2021-09-23
* build, deploy: Improve logging of image build messages [Paulo Castro]
* build, deploy: Add support for multiarch base images [toochevere]
## 12.48.15 - 2021-09-22
<details>
<summary> Update balena-sdk to 15.51.1 [Nitish Agarwal] </summary>
> ### balena-sdk-15.51.1 - 2021-09-20
>
>
> <details>
> <summary> Update balena-request to v11.4.2 [Kyle Harding] </summary>
>
>> #### balena-request-11.4.2 - 2021-09-20
>>
>> * Allow overriding the default zlib flush setting [Kyle Harding]
>>
> </details>
>
>
> ### balena-sdk-15.51.0 - 2021-09-16
>
> * os.getConfig: Add typings for the provisioningKeyName option [Nitish Agarwal]
>
> ### balena-sdk-15.50.1 - 2021-09-13
>
> * models/os: Always first normalize the device type slug [Thodoris Greasidis]
>
> ### balena-sdk-15.50.0 - 2021-09-10
>
> * Add release.finalize to promote draft releases to final [toochevere]
>
> ### balena-sdk-15.49.1 - 2021-09-10
>
> * typings: Drop the v5-model-only application_type.is_host_os [Thodoris Greasidis]
>
> ### balena-sdk-15.49.0 - 2021-09-06
>
> * os.getSupportedOsUpdateVersions: Use the hostApp releases [Thodoris Greasidis]
> * os.download: Use the hostApp for finding the latest release [Thodoris Greasidis]
>
> ### balena-sdk-15.48.3 - 2021-08-27
>
>
> <details>
> <summary> Update balena-request to 11.4.1 [Kyle Harding] </summary>
>
>> #### balena-request-11.4.1 - 2021-08-27
>>
>> * Allow more lenient gzip decompression [Kyle Harding]
>>
> </details>
>
>
> ### balena-sdk-15.48.2 - 2021-08-27
>
> * Improve hostapp.getAllOsVersions performance & reduce fetched data [Thodoris Greasidis]
>
> ### balena-sdk-15.48.1 - 2021-08-27
>
> * Update typescript to 4.4.2 [Thodoris Greasidis]
>
</details>
## 12.48.14 - 2021-09-20
* os download: Avoid incomplete os downloads appearing as successful [Kyle Harding]
## 12.48.13 - 2021-09-17
* config inject: Remove requirement of being logged in [Paulo Castro]
## 12.48.12 - 2021-09-10
* build/deploy: Update QEMU to 6.0.0 for emulated builds [Kyle Harding]
## 12.48.11 - 2021-09-10
* build, deploy: Fix processing of '--tag' option [Paulo Castro]
## 12.48.10 - 2021-09-10
* push: Await and retry supervisor API requests to a local device [Paulo Castro]
## 12.48.9 - 2021-09-09
* chore: Update net-keepalive dependency (fix CLI packaging errors) [Paulo Castro]
## 12.48.8 - 2021-09-08
* v13 preparations: Add feature switch for removal of '--gitignore' (push, build) [Paulo Castro]
* v13 preparations: Adjust test cases for 'balena envs' [Paulo Castro]
* v13 preparations: Adjust test cases for 'balena devices' [Paulo Castro]
## 12.48.7 - 2021-09-07
* device move: Improve types & reduce the number of API requests [Thodoris Greasidis]
* device move: Rely on the device type model to get the compatible apps [Thodoris Greasidis]
## 12.48.6 - 2021-09-07
* preload: Rely on the device type model to get the compatible apps [Thodoris Greasidis]
## 12.48.5 - 2021-09-07
* preload: Replace my_application query with the SDKs application.getAll() [Thodoris Greasidis]
## 12.48.4 - 2021-08-31
* os download: Use the hostApps instead of the device-types/v1 endpoint [Thodoris Greasidis]
## 12.48.3 - 2021-08-31
* balena deploy: Retrieve the cpu arch as part of the device type resource [Thodoris Greasidis]
## 12.48.2 - 2021-08-30
* Clarify installation instructions [Paulo Castro]
## 12.48.1 - 2021-08-27
* Improve error handling (remove most occurrences of process.exit()) [Paulo Castro]
* build, deploy: Extend CTRL-C coverage on Windows (PowerShell, cmd.exe) [Paulo Castro]
## 12.48.0 - 2021-08-26
* Add contract contents at release creation time [Paulo Castro]
* Fix env variable to avoid test failures [toochevere]
* Add balena.yml handling and `--draft` to `balena deploy` release creation [toochevere]
## 12.47.0 - 2021-08-19
* Add deprecation policy checker and --unsupported global flag [Paulo Castro]
## 12.46.2 - 2021-08-16
<details>
<summary> Update dependencies (balena-sdk from v15.36.0 to v15.48.0) [Paulo Castro] </summary>
> ### balena-sdk-15.48.0 - 2021-08-15
>
> * Deprecate the release.release_version property [Thodoris Greasidis]
> * typings: Add the release versioning properties [Thodoris Greasidis]
>
> ### balena-sdk-15.47.1 - 2021-08-10
>
> * Run browser tests using the minified browser bundle [Thodoris Greasidis]
> * Move to uglify-js to fix const assignment bug in minified build [Thodoris Greasidis]
>
> ### balena-sdk-15.47.0 - 2021-08-09
>
> * typings: Add the release.is_final & is_finalized_at__date properties [Thodoris Greasidis]
>
> ### balena-sdk-15.46.1 - 2021-07-28
>
> * apiKey.getAll: Return only NamedUserApiKeys for backwards compatibility [Thodoris Greasidis]
>
> ### balena-sdk-15.46.0 - 2021-07-27
>
> * Add email verification & email request methods [Nitish Agarwal]
>
> ### balena-sdk-15.45.0 - 2021-07-26
>
> * Update generateProvisioningKey to include keyName [Nitish Agarwal]
>
> ### balena-sdk-15.44.0 - 2021-07-15
>
> * typings: Add the subscription.is_active computed term [Thodoris Greasidis]
>
> ### balena-sdk-15.43.0 - 2021-07-14
>
> * typings: Add the organization_memebership.effective_seat_role field [Thodoris Greasidis]
>
> ### balena-sdk-15.42.2 - 2021-07-14
>
> * tests: Reduce the number of organizations created [Thodoris Greasidis]
>
> ### balena-sdk-15.42.1 - 2021-07-13
>
> * tests/api-key: Fix a race condition in the apiKey.create() tests [Thodoris Greasidis]
> * Convert the apiKey tests to async-await [Thodoris Greasidis]
>
> ### balena-sdk-15.42.0 - 2021-07-13
>
> * models/apiKey: Add getProvisioningApiKeysByApplication() method [Nitish Agarwal]
>
> ### balena-sdk-15.41.1 - 2021-06-30
>
> * Delete CODEOWNERS [Thodoris Greasidis]
>
> ### balena-sdk-15.41.0 - 2021-06-21
>
> * Add organization__has_private_access_to__device_type typings [Thodoris Greasidis]
> * typings: Add organization.has_past_due_invoice_since__date [Thodoris Greasidis]
>
> ### balena-sdk-15.40.0 - 2021-06-09
>
> * Add getAllNamedUserApiKeys() in the apiKey model [Thodoris Greasidis]
>
> ### balena-sdk-15.39.4 - 2021-06-08
>
> * Add missing modified_at in device type [JSReds]
>
> ### balena-sdk-15.39.3 - 2021-06-08
>
> * Fix lint with new linter version [JSReds]
>
> ### balena-sdk-15.39.2 - 2021-05-27
>
> * Update TypeScript to v4.3.2 [Thodoris Greasidis]
>
> ### balena-sdk-15.39.1 - 2021-05-24
>
> * Update balena-lint to v6 [Thodoris Greasidis]
>
> ### balena-sdk-15.39.0 - 2021-05-24
>
> * Add public device types [Tomás Migone]
>
> ### balena-sdk-15.38.0 - 2021-05-20
>
> * models/billing: Add changePlan method [Thodoris Greasidis]
> * Update DOCUMENTATION about getAllWithDeviceServiceDetails deprecation [Thodoris Greasidis]
>
> ### balena-sdk-15.37.0 - 2021-05-17
>
> * Add public organization types [JSReds]
>
</details>
## 12.46.1 - 2021-08-16
<details>
<summary> preload: Restore support for armv7 with custom preload image [Kyle Harding] </summary>
> ### balena-preload-10.5.0 - 2021-08-04
>
> * Remove mutually exclusive args from sfdisk [Kyle Harding]
> * Explicitly disable tls to avoid startup delays [Kyle Harding]
> * Use custom dind image based on alpine [Kyle Harding]
>
</details>
## 12.46.0 - 2021-08-15
* Add `--draft` option to `balena push` [Felipe Lalanne]
## 12.45.2 - 2021-08-13
* push, build: Improve error handling (identify which service failed) [Paulo Castro]
## 12.45.1 - 2021-08-11
* envs, env add: Fix "Application is ambiguous" when using device UUID [Paulo Castro]
## 12.45.0 - 2021-08-09 ## 12.45.0 - 2021-08-09
* Rename applications to fleets (stage 1). See: https://git.io/JRuZr [Paulo Castro] * Rename applications to fleets (stage 1). See: https://git.io/JRuZr [Paulo Castro]

View File

@ -259,3 +259,12 @@ gotchas to bear in mind:
`node_modules/balena-sdk/node_modules/balena-errors` `node_modules/balena-sdk/node_modules/balena-errors`
In the case of subclasses of `TypedError`, a string comparison may be used instead: In the case of subclasses of `TypedError`, a string comparison may be used instead:
`error.name === 'BalenaApplicationNotFound'` `error.name === 'BalenaApplicationNotFound'`
## Further debugging notes
* If you need to selectively run specific tests, `it.only` will not work in cases when authorization is required as part of the test cycle. In order to target specific tests, control execution via `.mocharc.js` instead. Here is an example of targeting the `deploy` tests.
replace: `spec: 'tests/**/*.spec.ts',`
with: `spec: ['tests/auth/*.spec.ts', 'tests/**/deploy.spec.ts'],`

View File

@ -1,6 +1,10 @@
# balena CLI Installation Instructions for Linux # balena CLI Installation Instructions for Linux
These instructions are suitable for most Linux distributions on Intel x86, except notably for **Linux Alpine** or **Busybox**. For these distros or for the ARM architecture, follow the [NPM Installation](./INSTALL-ADVANCED.md#npm-installation) method. These instructions are suitable for most Linux distributions on Intel x86, such as
Ubuntu, Debian, Fedora, Arch Linux and other glibc-based distributions.
For the ARM architecture and for Linux distributions not based on glibc, such as
Alpine Linux, follow the [NPM Installation](./INSTALL-ADVANCED.md#npm-installation)
method.
Selected operating system: **Linux** Selected operating system: **Linux**

View File

@ -10,16 +10,14 @@ Selected operating system: **macOS**
Look for a file name that ends with "-installer.pkg": Look for a file name that ends with "-installer.pkg":
`balena-cli-vX.Y.Z-macOS-x64-installer.pkg` `balena-cli-vX.Y.Z-macOS-x64-installer.pkg`
2. Double click the downloaded file to run the installer. After the installation completes, 2. Double click on the downloaded file to run the installer and follow the installer's
close and re-open any open [command instructions.
terminal](https://www.balena.io/docs/reference/cli/#choosing-a-shell-command-promptterminal)
windows (so that the changes made by the installer to the PATH environment variable can take
effect).
3. Check that the installation was successful by running the following commands on a 3. Check that the installation was successful:
command terminal: - [Open the Terminal
* `balena version` - should print the CLI's version app](https://support.apple.com/en-gb/guide/terminal/apd5265185d-f365-44cb-8b09-71a064a42125/mac).
* `balena help` - should print a list of available commands - On the terminal prompt, type `balena version` and hit Enter. It should display
the version of the balena CLI that you have installed.
No further steps are required to run most CLI commands. The `balena ssh`, `build`, `deploy` No further steps are required to run most CLI commands. The `balena ssh`, `build`, `deploy`
and `preload` commands may require additional software to be installed, as described and `preload` commands may require additional software to be installed, as described

View File

@ -10,16 +10,14 @@ Selected operating system: **Windows**
Look for a file name that ends with "-installer.exe": Look for a file name that ends with "-installer.exe":
`balena-cli-vX.Y.Z-windows-x64-installer.exe` `balena-cli-vX.Y.Z-windows-x64-installer.exe`
2. Double click the downloaded file to run the installer. After the installation completes, 2. Double click on the downloaded file to run the installer and follow the installer's
close and re-open any open [command instructions.
terminal](https://www.balena.io/docs/reference/cli/#choosing-a-shell-command-promptterminal)
windows (so that the changes made by the installer to the PATH environment variable can take
effect).
3. Check that the installation was successful by running the following commands on a 3. Check that the installation was successful:
command terminal: - Click on the Windows Start Menu, type PowerShell, and then click
* `balena version` - should print the CLI's version on Windows PowerShell.
* `balena help` - should print a list of available commands - On the command prompt, type `balena version` and hit Enter. It should display
the version of the balena CLI that you have installed.
No further steps are required to run most CLI commands. The `balena ssh`, `scan`, `build`, No further steps are required to run most CLI commands. The `balena ssh`, `scan`, `build`,
`deploy` and `preload` commands may require additional software to be installed, as `deploy` and `preload` commands may require additional software to be installed, as

View File

@ -156,14 +156,18 @@ of major, minor and patch version releases.
The latest release of a major version of the balena CLI will remain compatible with The latest release of a major version of the balena CLI will remain compatible with
the balenaCloud backend services for at least one year from the date when the the balenaCloud backend services for at least one year from the date when the
following major version is released. For example, balena CLI v10.17.5, as the following major version is released. For example, balena CLI v11.36.0, as the
latest v10 release, would remain compatible with the balenaCloud backend for one latest v11 release, would remain compatible with the balenaCloud backend for one
year from the date when v11.0.0 is released. year from the date when v12.0.0 was released.
At the end of this period, the older major version is considered deprecated and Half way through to that period (6 months after the release of the next major
some of the functionality that depends on balenaCloud services may stop working version), older major versions of the balena CLI will start printing a deprecation
at any time. warning message when it is used interactively (when `stderr` is attached to a TTY
Users are encouraged to regularly update the balena CLI to the latest version. device file). At the end of that period, older major versions will exit with an
error message unless the `--unsupported` flag is used. This behavior was
introduced in CLI version 12.47.0 and is also documented by `balena help`.
To take advantage of the latest backend features and ensure compatibility, users
are encouraged to regularly update the balena CLI to the latest version.
## Contributing (including editing documentation files) ## Contributing (including editing documentation files)

View File

@ -22,25 +22,26 @@ import * as archiver from 'archiver';
import * as Bluebird from 'bluebird'; import * as Bluebird from 'bluebird';
import { execFile } from 'child_process'; import { execFile } from 'child_process';
import * as filehound from 'filehound'; import * as filehound from 'filehound';
import { Stats } from 'fs';
import * as fs from 'fs-extra'; import * as fs from 'fs-extra';
import * as klaw from 'klaw';
import * as _ from 'lodash'; import * as _ from 'lodash';
import * as path from 'path'; import * as path from 'path';
import * as rimraf from 'rimraf'; import * as rimraf from 'rimraf';
import * as semver from 'semver'; import * as semver from 'semver';
import * as util from 'util'; import { promisify } from 'util';
import * as klaw from 'klaw';
import { Stats } from 'fs';
import { stripIndent } from '../lib/utils/lazy'; import { stripIndent } from '../build/utils/lazy';
import { import {
diffLines, diffLines,
getSubprocessStdout,
loadPackageJson, loadPackageJson,
ROOT, ROOT,
StdOutTap, StdOutTap,
whichSpawn, whichSpawn,
} from './utils'; } from './utils';
const execFileAsync = promisify(execFile);
export const packageJSON = loadPackageJson(); export const packageJSON = loadPackageJson();
export const version = 'v' + packageJSON.version; export const version = 'v' + packageJSON.version;
const arch = process.arch; const arch = process.arch;
@ -246,7 +247,17 @@ async function testPkg() {
console.log(`Testing standalone package "${pkgBalenaPath}"...`); console.log(`Testing standalone package "${pkgBalenaPath}"...`);
// Run `balena version -j`, parse its stdout as JSON, and check that the // Run `balena version -j`, parse its stdout as JSON, and check that the
// reported Node.js major version matches semver.major(process.version) // reported Node.js major version matches semver.major(process.version)
const stdout = await getSubprocessStdout(pkgBalenaPath, ['version', '-j']); let { stdout, stderr } = await execFileAsync(pkgBalenaPath, [
'version',
'-j',
]);
const { filterCliOutputForTests } = await import('../tests/helpers');
const filtered = filterCliOutputForTests({
err: stderr.split(/\r?\n/),
out: stdout.split(/\r?\n/),
});
stdout = filtered.out.join('\n');
stderr = filtered.err.join('\n');
let pkgNodeVersion = ''; let pkgNodeVersion = '';
let pkgNodeMajorVersion = 0; let pkgNodeMajorVersion = 0;
try { try {
@ -263,6 +274,10 @@ async function testPkg() {
`Mismatched major version: built-in pkg Node version="${pkgNodeVersion}" vs process.version="${process.version}"`, `Mismatched major version: built-in pkg Node version="${pkgNodeVersion}" vs process.version="${process.version}"`,
); );
} }
if (filtered.err.length > 0) {
const err = filtered.err.join('\n');
throw new Error(`"${pkgBalenaPath}": non-empty stderr "${err}"`);
}
console.log('Success! (standalone package test successful)'); console.log('Success! (standalone package test successful)');
} }
@ -411,8 +426,6 @@ async function renameInstallerFiles() {
async function signWindowsInstaller() { async function signWindowsInstaller() {
if (process.env.CSC_LINK && process.env.CSC_KEY_PASSWORD) { if (process.env.CSC_LINK && process.env.CSC_KEY_PASSWORD) {
const exeName = renamedOclifInstallers[process.platform]; const exeName = renamedOclifInstallers[process.platform];
const execFileAsync = util.promisify<string, string[], void>(execFile);
console.log(`Signing installer "${exeName}"`); console.log(`Signing installer "${exeName}"`);
await execFileAsync(MSYS2_BASH, [ await execFileAsync(MSYS2_BASH, [
'sign-exe.sh', 'sign-exe.sh',

View File

@ -82,6 +82,14 @@ const capitanoDoc = {
'build/commands/device/shutdown.js', 'build/commands/device/shutdown.js',
], ],
}, },
{
title: 'Releases',
files: [
'build/commands/releases.js',
'build/commands/release/index.js',
'build/commands/release/finalize.js',
],
},
{ {
title: 'Environment Variables', title: 'Environment Variables',
files: [ files: [

View File

@ -101,7 +101,7 @@ async function printMarkdown() {
console.log(await renderMarkdown()); console.log(await renderMarkdown());
} catch (error) { } catch (error) {
console.error(error); console.error(error);
process.exit(1); process.exitCode = 1;
} }
} }

View File

@ -41,17 +41,25 @@ function checkNpmVersion() {
// the reason is that it would unnecessarily prevent end users from // the reason is that it would unnecessarily prevent end users from
// using npm v6.4.1 that ships with Node 8. (It is OK for the // using npm v6.4.1 that ships with Node 8. (It is OK for the
// shrinkwrap file to get damaged if it is not going to be reused.) // shrinkwrap file to get damaged if it is not going to be reused.)
console.error(`\ throw new Error(`\
------------------------------------------------------------------------------- -----------------------------------------------------------------------------
Error: npm version '${npmVersion}' detected. Please upgrade to npm v${requiredVersion} or later Error: npm version '${npmVersion}' detected. Please upgrade to npm v${requiredVersion} or later
because of a bug that causes the 'npm-shrinkwrap.json' file to be damaged. because of a bug that causes the 'npm-shrinkwrap.json' file to be damaged.
At this point, however, your 'npm-shrinkwrap.json' file has already been At this point, however, your 'npm-shrinkwrap.json' file has already been
damaged. Please revert it to the master branch state with a command such as: damaged. Please revert it to the master branch state with a command such as:
"git checkout master -- npm-shrinkwrap.json" "git checkout master -- npm-shrinkwrap.json"
Then re-run "npm install" using npm version ${requiredVersion} or later. Then re-run "npm install" using npm version ${requiredVersion} or later.
-------------------------------------------------------------------------------`); -----------------------------------------------------------------------------`);
process.exit(1);
} }
} }
checkNpmVersion(); function main() {
try {
checkNpmVersion();
} catch (e) {
console.error(e.message || e);
process.exitCode = 1;
}
}
main();

View File

@ -54,9 +54,7 @@ export async function release() {
try { try {
await createGitHubRelease(); await createGitHubRelease();
} catch (err) { } catch (err) {
console.error('Release failed'); throw new Error(`Error creating GitHub release:\n${err}`);
console.error(err);
process.exit(1);
} }
} }

View File

@ -35,11 +35,6 @@ process.env.DEBUG = ['0', 'no', 'false', '', undefined].includes(
? '' ? ''
: '1'; : '1';
function exitWithError(error: Error | string): never {
console.error(`Error: ${error}`);
process.exit(1);
}
/** /**
* Trivial command-line parser. Check whether the command-line argument is one * Trivial command-line parser. Check whether the command-line argument is one
* of the following strings, then call the appropriate functions: * of the following strings, then call the appropriate functions:
@ -49,12 +44,12 @@ function exitWithError(error: Error | string): never {
* *
* @param args Arguments to parse (default is process.argv.slice(2)) * @param args Arguments to parse (default is process.argv.slice(2))
*/ */
export async function run(args?: string[]) { async function parse(args?: string[]) {
args = args || process.argv.slice(2); args = args || process.argv.slice(2);
console.log(`automation/run.ts process.argv=[${process.argv}]\n`); console.error(`[debug] automation/run.ts process.argv=[${process.argv}]`);
console.log(`automation/run.ts args=[${args}]`); console.error(`[debug] automation/run.ts args=[${args}]`);
if (_.isEmpty(args)) { if (_.isEmpty(args)) {
return exitWithError('missing command-line arguments'); throw new Error('missing command-line arguments');
} }
const commands: { [cmd: string]: () => void | Promise<void> } = { const commands: { [cmd: string]: () => void | Promise<void> } = {
'build:installer': buildOclifInstaller, 'build:installer': buildOclifInstaller,
@ -66,7 +61,7 @@ export async function run(args?: string[]) {
}; };
for (const arg of args) { for (const arg of args) {
if (!commands.hasOwnProperty(arg)) { if (!commands.hasOwnProperty(arg)) {
return exitWithError(`command unknown: ${arg}`); throw new Error(`command unknown: ${arg}`);
} }
} }
@ -90,9 +85,22 @@ export async function run(args?: string[]) {
const cmdFunc = commands[arg]; const cmdFunc = commands[arg];
await cmdFunc(); await cmdFunc();
} catch (err) { } catch (err) {
return exitWithError(`"${arg}": ${err}`); if (typeof err === 'object') {
err.message = `"${arg}": ${err.message}`;
}
throw err;
} }
} }
} }
/** See jsdoc for parse() function above */
export async function run(args?: string[]) {
try {
await parse(args);
} catch (e) {
console.error(e.message ? `Error: ${e.message}` : e);
process.exitCode = 1;
}
}
run(); run();

View File

@ -11,8 +11,7 @@ const validateChangeType = (maybeChangeType: string = 'minor') => {
case 'major': case 'major':
return maybeChangeType; return maybeChangeType;
default: default:
console.error(`Invalid change type: '${maybeChangeType}'`); throw new Error(`Invalid change type: '${maybeChangeType}'`);
return process.exit(1);
} }
}; };
@ -65,24 +64,17 @@ const getUpstreams = async () => {
return upstream; return upstream;
}; };
const printUsage = (upstreams: Upstream[], upstreamName: string) => { const getUsage = (upstreams: Upstream[], upstreamName: string) => `
console.error(
`
Usage: npm run update ${upstreamName} $version [$changeType=minor] Usage: npm run update ${upstreamName} $version [$changeType=minor]
Upstream names: ${upstreams.map(({ repo }) => repo).join(', ')} Upstream names: ${upstreams.map(({ repo }) => repo).join(', ')}
`, `;
);
return process.exit(1);
};
// TODO: Drop the wrapper function once we move to TS 3.8, async function $main() {
// which will support top level await.
async function main() {
const upstreams = await getUpstreams(); const upstreams = await getUpstreams();
if (process.argv.length < 3) { if (process.argv.length < 3) {
return printUsage(upstreams, '$upstreamName'); throw new Error(getUsage(upstreams, '$upstreamName'));
} }
const upstreamName = process.argv[2]; const upstreamName = process.argv[2];
@ -90,16 +82,15 @@ async function main() {
const upstream = upstreams.find((v) => v.repo === upstreamName); const upstream = upstreams.find((v) => v.repo === upstreamName);
if (!upstream) { if (!upstream) {
console.error( throw new Error(
`Invalid upstream name '${upstreamName}', valid options: ${upstreams `Invalid upstream name '${upstreamName}', valid options: ${upstreams
.map(({ repo }) => repo) .map(({ repo }) => repo)
.join(', ')}`, .join(', ')}`,
); );
return process.exit(1);
} }
if (process.argv.length < 4) { if (process.argv.length < 4) {
printUsage(upstreams, upstreamName); throw new Error(getUsage(upstreams, upstreamName));
} }
const packageName = upstream.module || upstream.repo; const packageName = upstream.module || upstream.repo;
@ -108,8 +99,7 @@ async function main() {
await run(`npm install ${packageName}@${process.argv[3]}`); await run(`npm install ${packageName}@${process.argv[3]}`);
const newVersion = await getVersion(packageName); const newVersion = await getVersion(packageName);
if (newVersion === oldVersion) { if (newVersion === oldVersion) {
console.error(`Already on version '${newVersion}'`); throw new Error(`Already on version '${newVersion}'`);
return process.exit(1);
} }
console.log(`Updated ${upstreamName} from ${oldVersion} to ${newVersion}`); console.log(`Updated ${upstreamName} from ${oldVersion} to ${newVersion}`);
@ -137,4 +127,13 @@ async function main() {
); );
} }
async function main() {
try {
await $main();
} catch (e) {
console.error(e);
process.exitCode = 1;
}
}
main(); main();

View File

@ -21,22 +21,6 @@ import * as path from 'path';
export const ROOT = path.join(__dirname, '..'); export const ROOT = path.join(__dirname, '..');
const nodeEngineWarn = `\
------------------------------------------------------------------------------
Warning: Node version "v14.x.x" does not match required versions ">=10.20.0 <13.0.0".
This may cause unexpected behavior. To upgrade Node, visit:
https://nodejs.org/en/download/
------------------------------------------------------------------------------
`;
const nodeEngineWarnArray = nodeEngineWarn.split('\n').filter((l) => l);
export function matchesNodeEngineVersionWarn(line: string) {
line = line.replace(/"v14\.\d{1,3}\.\d{1,3}"/, '"v14.x.x"');
return (
line === nodeEngineWarn || nodeEngineWarnArray.includes(line.trimEnd())
);
}
/** Tap and buffer this process' stdout and stderr */ /** Tap and buffer this process' stdout and stderr */
export class StdOutTap { export class StdOutTap {
public stdoutBuf: string[] = []; public stdoutBuf: string[] = [];
@ -104,60 +88,6 @@ export function loadPackageJson() {
return require(path.join(ROOT, 'package.json')); return require(path.join(ROOT, 'package.json'));
} }
/**
* Run the executable at execPath as a child process, and resolve a promise
* to the executable's stdout output as a string. Reject the promise if
* anything is printed to stderr, or if the child process exits with a
* non-zero exit code.
* @param execPath Executable path
* @param args Command-line argument for the executable
*/
export async function getSubprocessStdout(
execPath: string,
args: string[],
): Promise<string> {
const child = spawn(execPath, args);
return new Promise((resolve, reject) => {
let stdout = '';
child.stdout.on('error', reject);
child.stderr.on('error', reject);
child.stdout.on('data', (data: Buffer) => {
try {
stdout = data.toString();
} catch (err) {
reject(err);
}
});
child.stderr.on('data', (data: Buffer) => {
try {
const stderr = data.toString();
// ignore any debug lines, but ensure that we parse
// every line provided to the stderr stream
const lines = _.filter(
stderr.trim().split(/\r?\n/),
(line) =>
!line.startsWith('[debug]') && !matchesNodeEngineVersionWarn(line),
);
if (lines.length > 0) {
reject(
new Error(`"${execPath}": non-empty stderr "${lines.join('\n')}"`),
);
}
} catch (err) {
reject(err);
}
});
child.on('exit', (code: number) => {
if (code) {
reject(new Error(`"${execPath}": non-zero exit code "${code}"`));
} else {
resolve(stdout);
}
});
});
}
/** /**
* Error handling wrapper around the npm `which` package: * Error handling wrapper around the npm `which` package:
* "Like the unix which utility. Finds the first instance of a specified * "Like the unix which utility. Finds the first instance of a specified

View File

@ -8,7 +8,7 @@ _balena() {
local context state line curcontext="$curcontext" local context state line curcontext="$curcontext"
# Valid top-level completions # Valid top-level completions
main_commands=( apps build deploy envs fleets join keys leave login logout logs note orgs preload push scan settings ssh support tags tunnel version whoami api-key app app config device device devices env fleet fleet internal key key local os tag util ) main_commands=( apps build deploy envs fleets join keys leave login logout logs note orgs preload push releases scan settings ssh support tags tunnel version whoami api-key app app config device device devices env fleet fleet internal key key local os release release tag util )
# Sub-completions # Sub-completions
api_key_cmds=( generate ) api_key_cmds=( generate )
app_cmds=( create purge rename restart rm ) app_cmds=( create purge rename restart rm )
@ -21,6 +21,7 @@ _balena() {
key_cmds=( add rm ) key_cmds=( add rm )
local_cmds=( configure flash ) local_cmds=( configure flash )
os_cmds=( build-config configure download initialize versions ) os_cmds=( build-config configure download initialize versions )
release_cmds=( finalize )
tag_cmds=( rm set ) tag_cmds=( rm set )
@ -73,6 +74,9 @@ _balena_sec_cmds() {
"os") "os")
_describe -t os_cmds 'os_cmd' os_cmds "$@" && ret=0 _describe -t os_cmds 'os_cmd' os_cmds "$@" && ret=0
;; ;;
"release")
_describe -t release_cmds 'release_cmd' release_cmds "$@" && ret=0
;;
"tag") "tag")
_describe -t tag_cmds 'tag_cmd' tag_cmds "$@" && ret=0 _describe -t tag_cmds 'tag_cmd' tag_cmds "$@" && ret=0
;; ;;

View File

@ -7,7 +7,7 @@ _balena_complete()
local cur prev local cur prev
# Valid top-level completions # Valid top-level completions
main_commands="apps build deploy envs fleets join keys leave login logout logs note orgs preload push scan settings ssh support tags tunnel version whoami api-key app app config device device devices env fleet fleet internal key key local os tag util" main_commands="apps build deploy envs fleets join keys leave login logout logs note orgs preload push releases scan settings ssh support tags tunnel version whoami api-key app app config device device devices env fleet fleet internal key key local os release release tag util"
# Sub-completions # Sub-completions
api_key_cmds="generate" api_key_cmds="generate"
app_cmds="create purge rename restart rm" app_cmds="create purge rename restart rm"
@ -20,6 +20,7 @@ _balena_complete()
key_cmds="add rm" key_cmds="add rm"
local_cmds="configure flash" local_cmds="configure flash"
os_cmds="build-config configure download initialize versions" os_cmds="build-config configure download initialize versions"
release_cmds="finalize"
tag_cmds="rm set" tag_cmds="rm set"
@ -67,6 +68,9 @@ _balena_complete()
os) os)
COMPREPLY=( $(compgen -W "$os_cmds" -- $cur) ) COMPREPLY=( $(compgen -W "$os_cmds" -- $cur) )
;; ;;
release)
COMPREPLY=( $(compgen -W "$release_cmds" -- $cur) )
;;
tag) tag)
COMPREPLY=( $(compgen -W "$tag_cmds" -- $cur) ) COMPREPLY=( $(compgen -W "$tag_cmds" -- $cur) )
;; ;;

View File

@ -144,14 +144,18 @@ of major, minor and patch version releases.
The latest release of a major version of the balena CLI will remain compatible with The latest release of a major version of the balena CLI will remain compatible with
the balenaCloud backend services for at least one year from the date when the the balenaCloud backend services for at least one year from the date when the
following major version is released. For example, balena CLI v10.17.5, as the following major version is released. For example, balena CLI v11.36.0, as the
latest v10 release, would remain compatible with the balenaCloud backend for one latest v11 release, would remain compatible with the balenaCloud backend for one
year from the date when v11.0.0 is released. year from the date when v12.0.0 was released.
At the end of this period, the older major version is considered deprecated and Half way through to that period (6 months after the release of the next major
some of the functionality that depends on balenaCloud services may stop working version), older major versions of the balena CLI will start printing a deprecation
at any time. warning message when it is used interactively (when `stderr` is attached to a TTY
Users are encouraged to regularly update the balena CLI to the latest version. device file). At the end of that period, older major versions will exit with an
error message unless the `--unsupported` flag is used. This behavior was
introduced in CLI version 12.47.0 and is also documented by `balena help`.
To take advantage of the latest backend features and ensure compatibility, users
are encouraged to regularly update the balena CLI to the latest version.
# CLI Command Reference # CLI Command Reference
@ -203,6 +207,12 @@ Users are encouraged to regularly update the balena CLI to the latest version.
- [device rm &#60;uuid(s)&#62;](#device-rm-uuid-s) - [device rm &#60;uuid(s)&#62;](#device-rm-uuid-s)
- [device shutdown &#60;uuid&#62;](#device-shutdown-uuid) - [device shutdown &#60;uuid&#62;](#device-shutdown-uuid)
- Releases
- [releases &#60;fleet&#62;](#releases-fleet)
- [release &#60;commitorid&#62;](#release-commitorid)
- [release finalize &#60;commitorid&#62;](#release-finalize-commitorid)
- Environment Variables - Environment Variables
- [envs](#envs) - [envs](#envs)
@ -1290,6 +1300,80 @@ the uuid of the device to shutdown
force action if the update lock is set force action if the update lock is set
# Releases
## releases &#60;fleet&#62;
List all releases of the given fleet.
Fleets may be specified by fleet name or slug. Slugs are recommended because
they are unique and unambiguous. Slugs can be listed with the `balena fleets`
command. Note that slugs may change if the fleet is renamed. Fleet names are
not unique and may result in "Fleet is ambiguous" errors at any time (even if
"it used to work in the past"), for example if the name clashes with a newly
created public/open fleet, or with fleets from other balena accounts that you
may be invited to join under any role. For this reason, fleet names are
especially discouraged in scripts (e.g. CI environments).
Examples:
$ balena releases myorg/myfleet
### Arguments
#### FLEET
fleet name or slug
### Options
## release &#60;commitOrId&#62;
Examples:
$ balena release a777f7345fe3d655c1c981aa642e5555
$ balena release 1234567
### Arguments
#### COMMITORID
the commit or ID of the release to get information
### Options
#### -c, --composition
Return the release composition
## release finalize &#60;commitOrId&#62;
Finalize a release. Releases can be "draft" or "final", and this command
changes a draft release into a final release. Draft releases can be created
with the `--draft` option of the `balena build` or `balena deploy`
commands.
Draft releases are not automatically deployed to devices tracking the latest
release. For a draft release to be deployed to a device, the device should be
explicity pinned to that release. Conversely, final releases may trigger immediate
deployment to unpinned devices (subject to a device's polling period) and, for
this reason, final releases cannot be changed back to draft status.
Examples:
$ balena release finalize a777f7345fe3d655c1c981aa642e5555
$ balena release finalize 1234567
### Arguments
#### COMMITORID
the commit or ID of the release to finalize
### Options
# Environment Variables # Environment Variables
## envs ## envs
@ -2493,8 +2577,11 @@ the wifi key to use (used only if --network is set to wifi)
## config inject &#60;file&#62; ## config inject &#60;file&#62;
Inject a config.json file to the mounted filesystem, Inject a config.json file to a mounted filesystem, e.g. the SD card of a
e.g. the SD card of a provisioned device or balenaOS image. provisioned device or balenaOS image.
Note: if using a private/custom device type, please ensure you are logged in
('balena login' command). Public device types do not require logging in.
Examples: Examples:
@ -2922,22 +3009,29 @@ Don't convert line endings from CRLF (Windows format) to LF (Unix format).
Have each service use its own .dockerignore file. See "balena help push". Have each service use its own .dockerignore file. See "balena help push".
#### -G, --nogitignore
No-op (default behavior) since balena CLI v12.0.0. See "balena help push".
#### -g, --gitignore #### -g, --gitignore
Consider .gitignore files in addition to the .dockerignore file. This reverts Consider .gitignore files in addition to the .dockerignore file. This reverts
to the CLI v11 behavior/implementation (deprecated) if compatibility is to the CLI v11 behavior/implementation (deprecated) if compatibility is
required until your project can be adapted. required until your project can be adapted.
#### -G, --nogitignore
No-op (default behavior) since balena CLI v12.0.0. See "balena help push".
#### --release-tag RELEASE-TAG #### --release-tag RELEASE-TAG
Set release tags if the image build is successful (balenaCloud only). Multiple Set release tags if the image build is successful (balenaCloud only). Multiple
arguments may be provided, alternating tag keys and values (see examples). arguments may be provided, alternating tag keys and values (see examples).
Hint: Empty values may be specified with "" (bash, cmd.exe) or '""' (PowerShell). Hint: Empty values may be specified with "" (bash, cmd.exe) or '""' (PowerShell).
#### --draft
Instruct the builder to create the release as a draft. Draft releases are ignored
by the 'track latest' release policy but can be used through release pinning.
Draft releases can be marked as final through the API. Releases are created
as final by default unless this option is given.
# Settings # Settings
## settings ## settings
@ -3148,14 +3242,14 @@ Consider .gitignore files in addition to the .dockerignore file. This reverts
to the CLI v11 behavior/implementation (deprecated) if compatibility is required to the CLI v11 behavior/implementation (deprecated) if compatibility is required
until your project can be adapted. until your project can be adapted.
#### -m, --multi-dockerignore
Have each service use its own .dockerignore file. See "balena help build".
#### -G, --nogitignore #### -G, --nogitignore
No-op (default behavior) since balena CLI v12.0.0. See "balena help build". No-op (default behavior) since balena CLI v12.0.0. See "balena help build".
#### -m, --multi-dockerignore
Have each service use its own .dockerignore file. See "balena help build".
#### --noparent-check #### --noparent-check
Disable project validation check of 'docker-compose.yml' file in parent folder Disable project validation check of 'docker-compose.yml' file in parent folder
@ -3174,11 +3268,13 @@ Don't convert line endings from CRLF (Windows format) to LF (Unix format).
#### -n, --projectName PROJECTNAME #### -n, --projectName PROJECTNAME
Specify an alternate project name; default is the directory name Name prefix for locally built images. This is the 'projectName' portion
in 'projectName_serviceName:tag'. The default is the directory name.
#### -t, --tag TAG #### -t, --tag TAG
The alias to the generated image Tag locally built Docker images. This is the 'tag' portion
in 'projectName_serviceName:tag'. The default is 'latest'.
#### -B, --buildArg BUILDARG #### -B, --buildArg BUILDARG
@ -3358,6 +3454,13 @@ Set release tags if the image deployment is successful. Multiple
arguments may be provided, alternating tag keys and values (see examples). arguments may be provided, alternating tag keys and values (see examples).
Hint: Empty values may be specified with "" (bash, cmd.exe) or '""' (PowerShell). Hint: Empty values may be specified with "" (bash, cmd.exe) or '""' (PowerShell).
#### --draft
Deploy the release as a draft. Draft releases are ignored
by the 'track latest' release policy but can be used through release pinning.
Draft releases can be marked as final through the API. Releases are created
as final by default unless this option is given.
#### -e, --emulated #### -e, --emulated
Use QEMU for ARM architecture emulation during the image build Use QEMU for ARM architecture emulation during the image build
@ -3380,14 +3483,14 @@ Consider .gitignore files in addition to the .dockerignore file. This reverts
to the CLI v11 behavior/implementation (deprecated) if compatibility is required to the CLI v11 behavior/implementation (deprecated) if compatibility is required
until your project can be adapted. until your project can be adapted.
#### -m, --multi-dockerignore
Have each service use its own .dockerignore file. See "balena help build".
#### -G, --nogitignore #### -G, --nogitignore
No-op (default behavior) since balena CLI v12.0.0. See "balena help build". No-op (default behavior) since balena CLI v12.0.0. See "balena help build".
#### -m, --multi-dockerignore
Have each service use its own .dockerignore file. See "balena help build".
#### --noparent-check #### --noparent-check
Disable project validation check of 'docker-compose.yml' file in parent folder Disable project validation check of 'docker-compose.yml' file in parent folder
@ -3406,11 +3509,13 @@ Don't convert line endings from CRLF (Windows format) to LF (Unix format).
#### -n, --projectName PROJECTNAME #### -n, --projectName PROJECTNAME
Specify an alternate project name; default is the directory name Name prefix for locally built images. This is the 'projectName' portion
in 'projectName_serviceName:tag'. The default is the directory name.
#### -t, --tag TAG #### -t, --tag TAG
The alias to the generated image Tag locally built Docker images. This is the 'tag' portion
in 'projectName_serviceName:tag'. The default is 'latest'.
#### -B, --buildArg BUILDARG #### -B, --buildArg BUILDARG

View File

@ -16,8 +16,14 @@
*/ */
import * as packageJSON from '../package.json'; import * as packageJSON from '../package.json';
import {
AppOptions,
checkDeletedCommand,
preparseArgs,
unsupportedFlag,
} from './preparser';
import { CliSettings } from './utils/bootstrap'; import { CliSettings } from './utils/bootstrap';
import { onceAsync, stripIndent } from './utils/lazy'; import { onceAsync } from './utils/lazy';
/** /**
* Sentry.io setup * Sentry.io setup
@ -27,6 +33,7 @@ export const setupSentry = onceAsync(async () => {
const config = await import('./config'); const config = await import('./config');
const Sentry = await import('@sentry/node'); const Sentry = await import('@sentry/node');
Sentry.init({ Sentry.init({
autoSessionTracking: false,
dsn: config.sentryDsn, dsn: config.sentryDsn,
release: packageJSON.version, release: packageJSON.version,
}); });
@ -43,13 +50,8 @@ export const setupSentry = onceAsync(async () => {
async function checkNodeVersion() { async function checkNodeVersion() {
const validNodeVersions = packageJSON.engines.node; const validNodeVersions = packageJSON.engines.node;
if (!(await import('semver')).satisfies(process.version, validNodeVersions)) { if (!(await import('semver')).satisfies(process.version, validNodeVersions)) {
console.warn(stripIndent` const { getNodeEngineVersionWarn } = await import('./utils/messages');
------------------------------------------------------------------------------ console.warn(getNodeEngineVersionWarn(process.version, validNodeVersions));
Warning: Node version "${process.version}" does not match required versions "${validNodeVersions}".
This may cause unexpected behavior. To upgrade Node, visit:
https://nodejs.org/en/download/
------------------------------------------------------------------------------
`);
} }
} }
@ -93,10 +95,20 @@ async function init() {
} }
/** Execute the oclif parser and the CLI command. */ /** Execute the oclif parser and the CLI command. */
async function oclifRun( async function oclifRun(command: string[], options: AppOptions) {
command: string[], let deprecationPromise: Promise<void>;
options: import('./preparser').AppOptions, // check and enforce the CLI's deprecation policy
) { if (unsupportedFlag || process.env.BALENARC_UNSUPPORTED) {
deprecationPromise = Promise.resolve();
} else {
const { DeprecationChecker } = await import('./deprecation');
const deprecationChecker = new DeprecationChecker(packageJSON.version);
// warnAndAbortIfDeprecated uses previously cached data only
await deprecationChecker.warnAndAbortIfDeprecated();
// checkForNewReleasesIfNeeded may query the npm registry
deprecationPromise = deprecationChecker.checkForNewReleasesIfNeeded();
}
const runPromise = (async function (shouldFlush: boolean) { const runPromise = (async function (shouldFlush: boolean) {
const { CustomMain } = await import('./utils/oclif-utils'); const { CustomMain } = await import('./utils/oclif-utils');
let isEEXIT = false; let isEEXIT = false;
@ -130,14 +142,12 @@ async function oclifRun(
})(!options.noFlush); })(!options.noFlush);
const { trackPromise } = await import('./hooks/prerun/track'); const { trackPromise } = await import('./hooks/prerun/track');
await Promise.all([trackPromise, runPromise]);
await Promise.all([trackPromise, deprecationPromise, runPromise]);
} }
/** CLI entrypoint. Called by the `bin/balena` and `bin/balena-dev` scripts. */ /** CLI entrypoint. Called by the `bin/balena` and `bin/balena-dev` scripts. */
export async function run( export async function run(cliArgs = process.argv, options: AppOptions = {}) {
cliArgs = process.argv,
options: import('./preparser').AppOptions = {},
) {
try { try {
const { normalizeEnvVars, pkgExec } = await import('./utils/bootstrap'); const { normalizeEnvVars, pkgExec } = await import('./utils/bootstrap');
normalizeEnvVars(); normalizeEnvVars();
@ -150,8 +160,6 @@ export async function run(
await init(); await init();
const { preparseArgs, checkDeletedCommand } = await import('./preparser');
// Look for commands that have been removed and if so, exit with a notice // Look for commands that have been removed and if so, exit with a notice
checkDeletedCommand(cliArgs.slice(2)); checkDeletedCommand(cliArgs.slice(2));

View File

@ -120,7 +120,7 @@ export class FleetRenameCmd extends Command {
} catch (e) { } catch (e) {
// BalenaRequestError: Request error: "organization" and "app_name" must be unique. // BalenaRequestError: Request error: "organization" and "app_name" must be unique.
if ((e.message || '').toLowerCase().includes('unique')) { if ((e.message || '').toLowerCase().includes('unique')) {
throw new ExpectedError(`Error: fleet ${params.fleet} already exists.`); throw new ExpectedError(`Error: fleet ${newName} already exists.`);
} }
throw e; throw e;
} }

View File

@ -239,7 +239,12 @@ ${dockerignoreHelp}
) { ) {
const { loadProject } = await import('../utils/compose_ts'); const { loadProject } = await import('../utils/compose_ts');
const project = await loadProject(logger, composeOpts); const project = await loadProject(
logger,
composeOpts,
undefined,
opts.buildOpts.t,
);
const appType = (opts.app?.application_type as ApplicationType[])?.[0]; const appType = (opts.app?.application_type as ApplicationType[])?.[0];
if ( if (
@ -266,7 +271,7 @@ ${dockerignoreHelp}
inlineLogs: composeOpts.inlineLogs, inlineLogs: composeOpts.inlineLogs,
convertEol: composeOpts.convertEol, convertEol: composeOpts.convertEol,
dockerfilePath: composeOpts.dockerfilePath, dockerfilePath: composeOpts.dockerfilePath,
nogitignore: composeOpts.nogitignore, nogitignore: composeOpts.nogitignore, // v13: delete this line
multiDockerignore: composeOpts.multiDockerignore, multiDockerignore: composeOpts.multiDockerignore,
}); });
} }

View File

@ -34,8 +34,11 @@ export default class ConfigInjectCmd extends Command {
public static description = stripIndent` public static description = stripIndent`
Inject a configuration file into a device or OS image. Inject a configuration file into a device or OS image.
Inject a config.json file to the mounted filesystem, Inject a config.json file to a mounted filesystem, e.g. the SD card of a
e.g. the SD card of a provisioned device or balenaOS image. provisioned device or balenaOS image.
Note: if using a private/custom device type, please ensure you are logged in
('balena login' command). Public device types do not require logging in.
`; `;
public static examples = [ public static examples = [
@ -59,8 +62,6 @@ export default class ConfigInjectCmd extends Command {
help: cf.help, help: cf.help,
}; };
public static authenticated = true;
public static root = true; public static root = true;
public async run() { public async run() {

View File

@ -34,7 +34,7 @@ import type {
ComposeOpts, ComposeOpts,
Release as ComposeReleaseInfo, Release as ComposeReleaseInfo,
} from '../utils/compose-types'; } from '../utils/compose-types';
import type { DockerCliFlags } from '../utils/docker'; import type { BuildOpts, DockerCliFlags } from '../utils/docker';
import { import {
applyReleaseTagKeysAndValues, applyReleaseTagKeysAndValues,
buildProject, buildProject,
@ -59,6 +59,7 @@ interface FlagsDef extends ComposeCliFlags, DockerCliFlags {
build: boolean; build: boolean;
nologupload: boolean; nologupload: boolean;
'release-tag'?: string[]; 'release-tag'?: string[];
draft: boolean;
help: void; help: void;
} }
@ -136,6 +137,14 @@ ${dockerignoreHelp}
`, `,
multiple: true, multiple: true,
}), }),
draft: flags.boolean({
description: stripIndent`
Deploy the release as a draft. Draft releases are ignored
by the 'track latest' release policy but can be used through release pinning.
Draft releases can be marked as final through the API. Releases are created
as final by default unless this option is given.`,
default: false,
}),
...composeCliFlags, ...composeCliFlags,
...dockerCliFlags, ...dockerCliFlags,
// NOTE: Not supporting -h for help, because of clash with -h in DockerCliFlags // NOTE: Not supporting -h for help, because of clash with -h in DockerCliFlags
@ -213,6 +222,7 @@ ${dockerignoreHelp}
shouldPerformBuild: !!options.build, shouldPerformBuild: !!options.build,
shouldUploadLogs: !options.nologupload, shouldUploadLogs: !options.nologupload,
buildEmulated: !!options.emulated, buildEmulated: !!options.emulated,
createAsDraft: options.draft,
buildOpts, buildOpts,
}); });
await applyReleaseTagKeysAndValues( await applyReleaseTagKeysAndValues(
@ -235,7 +245,8 @@ ${dockerignoreHelp}
shouldPerformBuild: boolean; shouldPerformBuild: boolean;
shouldUploadLogs: boolean; shouldUploadLogs: boolean;
buildEmulated: boolean; buildEmulated: boolean;
buildOpts: any; // arguments to forward to docker build command buildOpts: BuildOpts;
createAsDraft: boolean;
}, },
) { ) {
const _ = await import('lodash'); const _ = await import('lodash');
@ -248,7 +259,12 @@ ${dockerignoreHelp}
const appType = (opts.app?.application_type as ApplicationType[])?.[0]; const appType = (opts.app?.application_type as ApplicationType[])?.[0];
try { try {
const project = await loadProject(logger, composeOpts, opts.image); const project = await loadProject(
logger,
composeOpts,
opts.image,
opts.buildOpts.t,
);
if (project.descriptors.length > 1 && !appType?.supports_multicontainer) { if (project.descriptors.length > 1 && !appType?.supports_multicontainer) {
throw new ExpectedError( throw new ExpectedError(
'Target fleet does not support multiple containers. Aborting!', 'Target fleet does not support multiple containers. Aborting!',
@ -303,7 +319,7 @@ ${dockerignoreHelp}
inlineLogs: composeOpts.inlineLogs, inlineLogs: composeOpts.inlineLogs,
convertEol: composeOpts.convertEol, convertEol: composeOpts.convertEol,
dockerfilePath: composeOpts.dockerfilePath, dockerfilePath: composeOpts.dockerfilePath,
nogitignore: composeOpts.nogitignore, nogitignore: composeOpts.nogitignore, // v13: delete this line
multiDockerignore: composeOpts.multiDockerignore, multiDockerignore: composeOpts.multiDockerignore,
}); });
builtImagesByService = _.keyBy(builtImages, 'serviceName'); builtImagesByService = _.keyBy(builtImages, 'serviceName');
@ -367,6 +383,8 @@ ${dockerignoreHelp}
`Bearer ${auth}`, `Bearer ${auth}`,
apiEndpoint, apiEndpoint,
!opts.shouldUploadLogs, !opts.shouldUploadLogs,
composeOpts.projectPath,
opts.createAsDraft,
); );
} }

View File

@ -17,7 +17,12 @@
import type { flags } from '@oclif/command'; import type { flags } from '@oclif/command';
import type { IArg } from '@oclif/parser/lib/args'; import type { IArg } from '@oclif/parser/lib/args';
import type { Application, BalenaSDK } from 'balena-sdk'; import type {
BalenaSDK,
Device,
DeviceType,
PineTypedResult,
} from 'balena-sdk';
import Command from '../../command'; import Command from '../../command';
import * as cf from '../../utils/common-flags'; import * as cf from '../../utils/common-flags';
import { ExpectedError } from '../../errors'; import { ExpectedError } from '../../errors';
@ -29,9 +34,12 @@ import {
} from '../../utils/messages'; } from '../../utils/messages';
import { isV13 } from '../../utils/version'; import { isV13 } from '../../utils/version';
interface ExtendedDevice extends DeviceWithDeviceType { type ExtendedDevice = PineTypedResult<
Device,
typeof import('../../utils/helpers').expandForAppNameAndCpuArch
> & {
application_name?: string; application_name?: string;
} };
interface FlagsDef { interface FlagsDef {
application?: string; application?: string;
@ -94,7 +102,7 @@ export default class DeviceMoveCmd extends Command {
const balena = getBalenaSdk(); const balena = getBalenaSdk();
const { tryAsInteger } = await import('../../utils/validation'); const { tryAsInteger } = await import('../../utils/validation');
const { expandForAppName } = await import('../../utils/helpers'); const { expandForAppNameAndCpuArch } = await import('../../utils/helpers');
// Parse ids string into array of correct types // Parse ids string into array of correct types
const deviceIds: Array<string | number> = params.uuid const deviceIds: Array<string | number> = params.uuid
@ -107,15 +115,14 @@ export default class DeviceMoveCmd extends Command {
(uuid) => (uuid) =>
balena.models.device.get( balena.models.device.get(
uuid, uuid,
expandForAppName, expandForAppNameAndCpuArch,
) as Promise<ExtendedDevice>, ) as Promise<ExtendedDevice>,
), ),
); );
// Map application name for each device // Map application name for each device
for (const device of devices) { for (const device of devices) {
const belongsToApplication = const belongsToApplication = device.belongs_to__application;
device.belongs_to__application as Application[];
device.application_name = belongsToApplication?.[0] device.application_name = belongsToApplication?.[0]
? belongsToApplication[0].app_name ? belongsToApplication[0].app_name
: 'N/a'; : 'N/a';
@ -145,42 +152,56 @@ export default class DeviceMoveCmd extends Command {
balena: BalenaSDK, balena: BalenaSDK,
devices: ExtendedDevice[], devices: ExtendedDevice[],
) { ) {
const [deviceDeviceTypes, deviceTypes] = await Promise.all([ const { getExpandedProp } = await import('../../utils/pine');
Promise.all( // deduplicate the slugs
devices.map((device) => const deviceCpuArchs = Array.from(
balena.models.device.getManifestBySlug( new Set(
device.is_of__device_type[0].slug, devices.map(
), (d) => d.is_of__device_type[0].is_of__cpu_architecture[0].slug,
), ),
), ),
balena.models.config.getDeviceTypes(), );
]);
const compatibleDeviceTypes = deviceTypes.filter((dt) => const deviceTypeOptions = {
deviceDeviceTypes.every( $select: 'slug',
(deviceDeviceType) => $expand: {
balena.models.os.isArchitectureCompatibleWith( is_of__cpu_architecture: {
deviceDeviceType.arch, $select: 'slug',
dt.arch, },
) && },
!!dt.isDependent === !!deviceDeviceType.isDependent && } as const;
dt.state !== 'DISCONTINUED', const deviceTypes = (await balena.models.deviceType.getAllSupported(
), deviceTypeOptions,
)) as Array<PineTypedResult<DeviceType, typeof deviceTypeOptions>>;
const compatibleDeviceTypeSlugs = new Set(
deviceTypes
.filter((deviceType) => {
const deviceTypeArch = getExpandedProp(
deviceType.is_of__cpu_architecture,
'slug',
)!;
return deviceCpuArchs.every((deviceCpuArch) =>
balena.models.os.isArchitectureCompatibleWith(
deviceCpuArch,
deviceTypeArch,
),
);
})
.map((deviceType) => deviceType.slug),
); );
const patterns = await import('../../utils/patterns'); const patterns = await import('../../utils/patterns');
try { try {
const application = await patterns.selectApplication( const application = await patterns.selectApplication(
(app) => (app) =>
compatibleDeviceTypes.some( compatibleDeviceTypeSlugs.has(app.is_for__device_type[0].slug) &&
(dt) => dt.slug === app.is_for__device_type[0].slug,
) &&
devices.some((device) => device.application_name !== app.app_name), devices.some((device) => device.application_name !== app.app_name),
true, true,
); );
return application; return application;
} catch (err) { } catch (err) {
if (deviceDeviceTypes.length) { if (!compatibleDeviceTypeSlugs.size) {
throw new ExpectedError( throw new ExpectedError(
`${err.message}\nDo all devices have a compatible architecture?`, `${err.message}\nDo all devices have a compatible architecture?`,
); );

View File

@ -227,7 +227,7 @@ async function setServiceVars(
sdk, sdk,
uuid, uuid,
['id'], ['id'],
['app_name'], ['slug'],
); );
} catch (err) { } catch (err) {
console.error(`${err.message}, device: ${uuid}`); console.error(`${err.message}, device: ${uuid}`);
@ -236,11 +236,7 @@ async function setServiceVars(
} }
for (const service of options.service!.split(',')) { for (const service of options.service!.split(',')) {
try { try {
const serviceId = await getServiceIdForApp( const serviceId = await getServiceIdForApp(sdk, app.slug, service);
sdk,
app.app_name,
service,
);
await sdk.models.device.serviceVar.set( await sdk.models.device.serviceVar.set(
device.id, device.id,
serviceId, serviceId,

View File

@ -91,8 +91,6 @@ export default class EnvRmCmd extends Command {
await confirm( await confirm(
opt.yes || false, opt.yes || false,
'Are you sure you want to delete the environment variable?', 'Are you sure you want to delete the environment variable?',
undefined,
true,
); );
const balena = getBalenaSdk(); const balena = getBalenaSdk();

View File

@ -174,11 +174,11 @@ export default class EnvsCmd extends Command {
balena, balena,
options.device, options.device,
['uuid'], ['uuid'],
['app_name'], ['slug'],
); );
fullUUID = device.uuid; fullUUID = device.uuid;
if (app) { if (app) {
appNameOrSlug = app.app_name; appNameOrSlug = app.slug;
} }
} }
if (appNameOrSlug && options.service) { if (appNameOrSlug && options.service) {
@ -210,7 +210,14 @@ export default class EnvsCmd extends Command {
// Replace undefined app names with 'N/A' or null // Replace undefined app names with 'N/A' or null
varArray = varArray.map((i: EnvironmentVariableInfo) => { varArray = varArray.map((i: EnvironmentVariableInfo) => {
i.appName = i.appName || (options.json ? null : 'N/A'); if (i.appName) {
// use slug in v13, app name in v12 for compatibility
i.appName = isV13()
? i.appName
: i.appName.substring(i.appName.indexOf('/') + 1);
} else {
i.appName = options.json ? null : 'N/A';
}
return i; return i;
}); });

View File

@ -20,12 +20,7 @@ import type { BlockDevice } from 'etcher-sdk/build/source-destination';
import Command from '../../command'; import Command from '../../command';
import { ExpectedError } from '../../errors'; import { ExpectedError } from '../../errors';
import * as cf from '../../utils/common-flags'; import * as cf from '../../utils/common-flags';
import { import { getChalk, getVisuals, stripIndent } from '../../utils/lazy';
getChalk,
getCliForm,
getVisuals,
stripIndent,
} from '../../utils/lazy';
interface FlagsDef { interface FlagsDef {
yes: boolean; yes: boolean;
@ -93,24 +88,15 @@ export default class LocalFlashCmd extends Command {
} }
} }
const { sourceDestination, multiWrite } = await import('etcher-sdk');
const drive = await this.getDrive(options); const drive = await this.getDrive(options);
const yes = const { confirm } = await import('../../utils/patterns');
options.yes || await confirm(
(await getCliForm().ask({ options.yes,
message: 'This will erase the selected drive. Are you sure?', 'This will erase the selected drive. Are you sure?',
type: 'confirm', );
name: 'yes',
default: false,
}));
if (!yes) {
console.log(getChalk().red.bold('Aborted image flash'));
process.exit(0);
}
const { sourceDestination, multiWrite } = await import('etcher-sdk');
const file = new sourceDestination.File({ const file = new sourceDestination.File({
path: params.image, path: params.image,
}); });

View File

@ -92,7 +92,6 @@ export default class OsInitializeCmd extends Command {
options.yes, options.yes,
`This will erase ${answers.drive}. Are you sure?`, `This will erase ${answers.drive}. Are you sure?`,
`Going to erase ${answers.drive}.`, `Going to erase ${answers.drive}.`,
true,
); );
const { safeUmount } = await import('../../utils/umount'); const { safeUmount } = await import('../../utils/umount');
await safeUmount(answers.drive); await safeUmount(answers.drive);

View File

@ -36,13 +36,7 @@ import { isV13 } from '../utils/version';
import { flags } from '@oclif/command'; import { flags } from '@oclif/command';
import * as _ from 'lodash'; import * as _ from 'lodash';
import type { import type { Application, BalenaSDK, PineExpand, Release } from 'balena-sdk';
Application,
BalenaSDK,
DeviceTypeJson,
PineExpand,
Release,
} from 'balena-sdk';
import type { Preloader } from 'balena-preload'; import type { Preloader } from 'balena-preload';
interface FlagsDef extends DockerConnectionCliFlags { interface FlagsDef extends DockerConnectionCliFlags {
@ -331,7 +325,6 @@ Can be repeated to add multiple certificates.\
readonly applicationExpandOptions: PineExpand<Application> = { readonly applicationExpandOptions: PineExpand<Application> = {
owns__release: { owns__release: {
$select: ['id', 'commit', 'end_timestamp', 'composition'], $select: ['id', 'commit', 'end_timestamp', 'composition'],
$orderby: [{ end_timestamp: 'desc' }, { id: 'desc' }],
$expand: { $expand: {
contains__image: { contains__image: {
$select: ['image'], $select: ['image'],
@ -345,77 +338,75 @@ Can be repeated to add multiple certificates.\
$filter: { $filter: {
status: 'success', status: 'success',
}, },
$orderby: [{ end_timestamp: 'desc' }, { id: 'desc' }],
}, },
should_be_running__release: { should_be_running__release: {
$select: 'commit', $select: 'commit',
}, },
}; };
allDeviceTypes: DeviceTypeJson.DeviceType[];
async getDeviceTypes() {
if (this.allDeviceTypes === undefined) {
const balena = getBalenaSdk();
const deviceTypes = await balena.models.config.getDeviceTypes();
this.allDeviceTypes = _.sortBy(deviceTypes, 'name');
}
return this.allDeviceTypes;
}
isCurrentCommit(commit: string) { isCurrentCommit(commit: string) {
return commit === 'latest' || commit === 'current'; return commit === 'latest' || commit === 'current';
} }
async getDeviceTypesWithSameArch(deviceTypeSlug: string) {
const deviceTypes = await this.getDeviceTypes();
const deviceType = _.find(deviceTypes, { slug: deviceTypeSlug });
if (!deviceType) {
throw new Error(`Device type "${deviceTypeSlug}" not found in API query`);
}
return _(deviceTypes).filter({ arch: deviceType.arch }).map('slug').value();
}
async getApplicationsWithSuccessfulBuilds(deviceTypeSlug: string) { async getApplicationsWithSuccessfulBuilds(deviceTypeSlug: string) {
const balena = getBalenaSdk(); const balena = getBalenaSdk();
const deviceTypes = await this.getDeviceTypesWithSameArch(deviceTypeSlug); try {
// TODO: remove the explicit types once https://github.com/balena-io/balena-sdk/pull/889 gets merged await balena.models.deviceType.get(deviceTypeSlug);
return balena.pine.get< } catch {
Application, throw new Error(`Device type "${deviceTypeSlug}" not found in API query`);
Array< }
ApplicationWithDeviceType & { return (await balena.models.application.getAll({
should_be_running__release: [Release?]; $select: ['id', 'app_name', 'should_track_latest_release'],
} $expand: this.applicationExpandOptions,
> $filter: {
>({ // get the apps that are of the same arch as the device type of the image
resource: 'my_application', is_for__device_type: {
options: { $any: {
$filter: { $alias: 'dt',
is_for__device_type: { $expr: {
$any: { dt: {
$alias: 'dt', is_of__cpu_architecture: {
$expr: { $any: {
dt: { $alias: 'ioca',
slug: { $in: deviceTypes }, $expr: {
}, ioca: {
}, is_supported_by__device_type: {
}, $any: {
}, $alias: 'isbdt',
owns__release: { $expr: {
$any: { isbdt: {
$alias: 'r', slug: deviceTypeSlug,
$expr: { },
r: { },
status: 'success', },
},
},
},
},
}, },
}, },
}, },
}, },
}, },
$expand: this.applicationExpandOptions, owns__release: {
$select: ['id', 'app_name', 'should_track_latest_release'], $any: {
$orderby: 'app_name asc', $alias: 'r',
$expr: {
r: {
status: 'success',
},
},
},
},
}, },
}); $orderby: 'app_name asc',
})) as Array<
ApplicationWithDeviceType & {
should_be_running__release: [Release?];
}
>;
} }
async selectApplication(deviceTypeSlug: string) { async selectApplication(deviceTypeSlug: string) {

View File

@ -47,8 +47,8 @@ interface FlagsDef {
pull: boolean; pull: boolean;
'noparent-check': boolean; 'noparent-check': boolean;
'registry-secrets'?: string; 'registry-secrets'?: string;
gitignore?: boolean; gitignore?: boolean; // v13: delete this flag
nogitignore?: boolean; nogitignore?: boolean; // v13: delete this flag
nolive: boolean; nolive: boolean;
detached: boolean; detached: boolean;
service?: string[]; service?: string[];
@ -58,6 +58,7 @@ interface FlagsDef {
'noconvert-eol': boolean; 'noconvert-eol': boolean;
'multi-dockerignore': boolean; 'multi-dockerignore': boolean;
'release-tag'?: string[]; 'release-tag'?: string[];
draft: boolean;
help: void; help: void;
} }
@ -236,11 +237,20 @@ export default class PushCmd extends Command {
'Have each service use its own .dockerignore file. See "balena help push".', 'Have each service use its own .dockerignore file. See "balena help push".',
char: 'm', char: 'm',
default: false, default: false,
exclusive: ['gitignore'], exclusive: ['gitignore'], // v13: delete this line
}), }),
...(isV13() ...(isV13()
? {} ? {}
: { : {
gitignore: flags.boolean({
description: stripIndent`
Consider .gitignore files in addition to the .dockerignore file. This reverts
to the CLI v11 behavior/implementation (deprecated) if compatibility is
required until your project can be adapted.`,
char: 'g',
default: false,
exclusive: ['multi-dockerignore'],
}),
nogitignore: flags.boolean({ nogitignore: flags.boolean({
description: description:
'No-op (default behavior) since balena CLI v12.0.0. See "balena help push".', 'No-op (default behavior) since balena CLI v12.0.0. See "balena help push".',
@ -249,15 +259,6 @@ export default class PushCmd extends Command {
default: false, default: false,
}), }),
}), }),
gitignore: flags.boolean({
description: stripIndent`
Consider .gitignore files in addition to the .dockerignore file. This reverts
to the CLI v11 behavior/implementation (deprecated) if compatibility is
required until your project can be adapted.`,
char: 'g',
default: false,
exclusive: ['multi-dockerignore'],
}),
'release-tag': flags.string({ 'release-tag': flags.string({
description: stripIndent` description: stripIndent`
Set release tags if the image build is successful (balenaCloud only). Multiple Set release tags if the image build is successful (balenaCloud only). Multiple
@ -267,6 +268,14 @@ export default class PushCmd extends Command {
multiple: true, multiple: true,
exclusive: ['detached'], exclusive: ['detached'],
}), }),
draft: flags.boolean({
description: stripIndent`
Instruct the builder to create the release as a draft. Draft releases are ignored
by the 'track latest' release policy but can be used through release pinning.
Draft releases can be marked as final through the API. Releases are created
as final by default unless this option is given.`,
default: false,
}),
help: cf.help, help: cf.help,
}; };
@ -362,13 +371,14 @@ export default class PushCmd extends Command {
registrySecrets, registrySecrets,
headless: options.detached, headless: options.detached,
convertEol: !options['noconvert-eol'], convertEol: !options['noconvert-eol'],
isDraft: options.draft,
}; };
const args = { const args = {
appSlug: application.slug, appSlug: application.slug,
source: options.source, source: options.source,
auth: token, auth: token,
baseUrl, baseUrl,
nogitignore: !options.gitignore, nogitignore: !options.gitignore, // v13: delete this line
sdk, sdk,
opts, opts,
}; };
@ -394,7 +404,7 @@ export default class PushCmd extends Command {
registrySecrets: RegistrySecrets, registrySecrets: RegistrySecrets,
) { ) {
// Check for invalid options // Check for invalid options
const remoteOnlyOptions: Array<keyof FlagsDef> = ['release-tag']; const remoteOnlyOptions: Array<keyof FlagsDef> = ['release-tag', 'draft'];
this.checkInvalidOptions( this.checkInvalidOptions(
remoteOnlyOptions, remoteOnlyOptions,
options, options,
@ -412,7 +422,7 @@ export default class PushCmd extends Command {
multiDockerignore: options['multi-dockerignore'], multiDockerignore: options['multi-dockerignore'],
nocache: options.nocache, nocache: options.nocache,
pull: options.pull, pull: options.pull,
nogitignore: !options.gitignore, nogitignore: !options.gitignore, // v13: delete this line
noParentCheck: options['noparent-check'], noParentCheck: options['noparent-check'],
nolive: options.nolive, nolive: options.nolive,
detached: options.detached, detached: options.detached,

View File

@ -0,0 +1,86 @@
/**
* @license
* Copyright 2016-2020 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.
*/
import { flags } from '@oclif/command';
import Command from '../../command';
import * as cf from '../../utils/common-flags';
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
import { tryAsInteger } from '../../utils/validation';
interface FlagsDef {
help: void;
}
interface ArgsDef {
commitOrId: string | number;
}
export default class ReleaseFinalizeCmd extends Command {
public static description = stripIndent`
Finalize a release.
Finalize a release. Releases can be "draft" or "final", and this command
changes a draft release into a final release. Draft releases can be created
with the \`--draft\` option of the \`balena build\` or \`balena deploy\`
commands.
Draft releases are not automatically deployed to devices tracking the latest
release. For a draft release to be deployed to a device, the device should be
explicity pinned to that release. Conversely, final releases may trigger immediate
deployment to unpinned devices (subject to a device's polling period) and, for
this reason, final releases cannot be changed back to draft status.
`;
public static examples = [
'$ balena release finalize a777f7345fe3d655c1c981aa642e5555',
'$ balena release finalize 1234567',
];
public static usage = 'release finalize <commitOrId>';
public static flags: flags.Input<FlagsDef> = {
help: cf.help,
};
public static args = [
{
name: 'commitOrId',
description: 'the commit or ID of the release to finalize',
required: true,
parse: (commitOrId: string) => tryAsInteger(commitOrId),
},
];
public static authenticated = true;
public async run() {
const { args: params } = this.parse<FlagsDef, ArgsDef>(ReleaseFinalizeCmd);
const balena = getBalenaSdk();
const release = await balena.models.release.get(params.commitOrId, {
$select: ['id', 'is_final'],
});
if (release.is_final) {
console.log(`Release ${params.commitOrId} is already finalized!`);
return;
}
await balena.models.release.finalize(release.id);
console.log(`Release ${params.commitOrId} finalized`);
}
}

View File

@ -0,0 +1,128 @@
/**
* @license
* Copyright 2016-2020 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.
*/
import { flags } from '@oclif/command';
import Command from '../../command';
import * as cf from '../../utils/common-flags';
import { getBalenaSdk, getVisuals, stripIndent } from '../../utils/lazy';
import type * as BalenaSdk from 'balena-sdk';
import jsyaml = require('js-yaml');
import { tryAsInteger } from '../../utils/validation';
interface FlagsDef {
help: void;
composition?: boolean;
}
interface ArgsDef {
commitOrId: string | number;
}
export default class ReleaseCmd extends Command {
public static description = stripIndent`
Get info for a release.
`;
public static examples = [
'$ balena release a777f7345fe3d655c1c981aa642e5555',
'$ balena release 1234567',
];
public static usage = 'release <commitOrId>';
public static flags: flags.Input<FlagsDef> = {
help: cf.help,
composition: flags.boolean({
default: false,
char: 'c',
description: 'Return the release composition',
}),
};
public static args = [
{
name: 'commitOrId',
description: 'the commit or ID of the release to get information',
required: true,
parse: (commitOrId: string) => tryAsInteger(commitOrId),
},
];
public static authenticated = true;
public async run() {
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
ReleaseCmd,
);
const balena = getBalenaSdk();
if (options.composition) {
await this.showComposition(params.commitOrId, balena);
} else {
await this.showReleaseInfo(params.commitOrId, balena);
}
}
async showComposition(
commitOrId: string | number,
balena: BalenaSdk.BalenaSDK,
) {
const release = await balena.models.release.get(commitOrId, {
$select: 'composition',
});
console.log(jsyaml.dump(release.composition));
}
async showReleaseInfo(
commitOrId: string | number,
balena: BalenaSdk.BalenaSDK,
) {
const fields: Array<keyof BalenaSdk.Release> = [
'id',
'commit',
'created_at',
'status',
'semver',
'is_final',
'build_log',
'start_timestamp',
'end_timestamp',
];
const release = await balena.models.release.get(commitOrId, {
$select: fields,
$expand: {
release_tag: {
$select: ['tag_key', 'value'],
},
},
});
const tagStr = release
.release_tag!.map((t) => `${t.tag_key}=${t.value}`)
.join('\n');
const _ = await import('lodash');
const values = _.mapValues(
release,
(val) => val ?? 'N/a',
) as Dictionary<string>;
values['tags'] = tagStr;
console.log(getVisuals().table.vertical(values, [...fields, 'tags']));
}
}

86
lib/commands/releases.ts Normal file
View File

@ -0,0 +1,86 @@
/**
* @license
* Copyright 2016-2020 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.
*/
import { flags } from '@oclif/command';
import Command from '../command';
import * as cf from '../utils/common-flags';
import { getBalenaSdk, getVisuals, stripIndent } from '../utils/lazy';
import { applicationNameNote } from '../utils/messages';
import type * as BalenaSdk from 'balena-sdk';
interface FlagsDef {
help: void;
}
interface ArgsDef {
fleet: string;
}
export default class ReleasesCmd extends Command {
public static description = stripIndent`
List all releases of a fleet.
List all releases of the given fleet.
${applicationNameNote.split('\n').join('\n\t\t')}
`;
public static examples = ['$ balena releases myorg/myfleet'];
public static usage = 'releases <fleet>';
public static flags: flags.Input<FlagsDef> = {
help: cf.help,
};
public static args = [
{
name: 'fleet',
description: 'fleet name or slug',
required: true,
},
];
public static authenticated = true;
public async run() {
const { args: params } = this.parse<FlagsDef, ArgsDef>(ReleasesCmd);
const fields: Array<keyof BalenaSdk.Release> = [
'id',
'commit',
'created_at',
'status',
'semver',
'is_final',
];
const balena = getBalenaSdk();
const releases = await balena.models.release.getAllByApplication(
params.fleet,
{ $select: fields },
);
const _ = await import('lodash');
console.log(
getVisuals().table.horizontal(
releases.map((rel) => _.mapValues(rel, (val) => val ?? 'N/a')),
fields,
),
);
}
}

230
lib/deprecation.ts Normal file
View File

@ -0,0 +1,230 @@
/**
* @license
* Copyright 2021 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.
*/
export interface ReleaseTimestampsByVersion {
[version: string]: string; // e.g. { '12.0.0': '2021-06-16T12:54:52.000Z' }
lastFetched: string; // ISO 8601 timestamp, e.g. '2021-06-27T16:46:10.000Z'
}
/**
* Warn about and enforce the CLI deprecation policy stated in the README
* file. In particular:
* The latest release of a major version will remain compatible with
* the backend services for at least one year from the date when the
* following major version is released. [...]
* Half way through to that period (6 months), old major versions of the
* balena CLI will start printing a deprecation warning message.
* At the end of that period, older major versions will abort with an error
* message unless the `--unsupported` flag is used.
*
* - Check for new balena-cli releases by querying the npm registry.
* - Cache results for a number of days to improve performance.
*
* For this feature's specification and planning, see (restricted access):
* https://jel.ly.fish/ed8d2395-9323-418c-bb67-d11d32a17d00
*/
export class DeprecationChecker {
readonly majorVersionFetchIntervalDays = 7;
readonly expiryDays = 365;
readonly deprecationDays = Math.ceil(this.expiryDays / 2);
readonly msInDay = 24 * 60 * 60 * 1000; // milliseconds in a day
readonly debugPrefix = 'Deprecation check';
readonly cacheFile = 'cachedReleaseTimestamps';
readonly now = new Date().getTime();
private initialized = false;
storage: ReturnType<typeof import('balena-settings-storage')>;
cachedTimestamps: ReleaseTimestampsByVersion;
nextMajorVersion: string; // semver without the 'v' prefix
constructor(protected currentVersion: string) {
const semver = require('semver') as typeof import('semver');
const major = semver.major(this.currentVersion, { loose: true });
this.nextMajorVersion = `${major + 1}.0.0`;
}
public async init() {
if (this.initialized) {
return;
}
this.initialized = true;
const settings = await import('balena-settings-client');
const getStorage = await import('balena-settings-storage');
const dataDirectory = settings.get<string>('dataDirectory');
this.storage = getStorage({ dataDirectory });
let stored: ReleaseTimestampsByVersion | undefined;
try {
stored = (await this.storage.get(
this.cacheFile,
)) as ReleaseTimestampsByVersion;
} catch {
// ignore
}
this.cachedTimestamps = {
...stored,
// '1970-01-01T00:00:00.000Z' is new Date(0).toISOString()
lastFetched: stored?.lastFetched || '1970-01-01T00:00:00.000Z',
};
}
/**
* Get NPM registry URL to retrieve the package.json file for a given version.
* @param version Semver without 'v' prefix, e.g. '12.0.0.'
*/
protected getNpmUrl(version: string) {
return `http://registry.npmjs.org/balena-cli/${version}`;
}
/**
* Query the npm registry (HTTP request) for a given balena-cli version.
*
* @param version semver version without the 'v' prefix, e.g. '13.0.0'
* @returns `undefined` if the request status code is 404 (version not
* published), otherwise a publishedAt date in ISO 8601 format, e.g.
* '2021-06-27T16:46:10.000Z'.
*/
protected async fetchPublishedTimestampForVersion(
version: string,
): Promise<string | undefined> {
const { default: got } = await import('got');
const url = this.getNpmUrl(version);
let response: import('got').Response<Dictionary<any>> | undefined;
try {
response = await got(url, { responseType: 'json', retry: 0 });
} catch (e) {
// 404 is expected if `version` hasn't been published yet
if (e.response?.statusCode !== 404) {
throw new Error(`Failed to query "${url}":\n${e}`);
}
}
// response.body looks like a package.json file, plus possibly a
// `versionist.publishedAt` field added by `github.com/product-os/versionist`
const publishedAt: string | undefined =
response?.body?.versionist?.publishedAt;
if (!publishedAt && process.env.DEBUG) {
console.error(`\
[debug] ${this.debugPrefix}: balena CLI next major version "${this.nextMajorVersion}" not released, \
or release date not available`);
}
return publishedAt; // ISO 8601, e.g. '2021-06-27T16:46:10.000Z'
}
/**
* Check if we already know (cached value) when the next major version
* was released. If we don't know, check how long ago the npm registry
* was last fetched, and fetch again if it has been longer than
* `majorVersionFetchIntervalDays`.
*/
public async checkForNewReleasesIfNeeded() {
if (process.env.BALENARC_UNSUPPORTED) {
return; // for the benefit of code testing
}
await this.init();
if (this.cachedTimestamps[this.nextMajorVersion]) {
// A cached value exists: no need to check the npm registry
return;
}
const lastFetched = new Date(this.cachedTimestamps.lastFetched).getTime();
const daysSinceLastFetch = (this.now - lastFetched) / this.msInDay;
if (daysSinceLastFetch < this.majorVersionFetchIntervalDays) {
if (process.env.DEBUG) {
// toFixed(5) results in a precision of ~1 second
const days = daysSinceLastFetch.toFixed(5);
console.error(`\
[debug] ${this.debugPrefix}: ${days} days since last npm registry query for next major version release date.
[debug] Will not query the registry again until at least ${this.majorVersionFetchIntervalDays} days have passed.`);
}
return;
}
if (process.env.DEBUG) {
console.error(`\
[debug] ${
this.debugPrefix
}: Cache miss for the balena CLI next major version release date.
[debug] Will query ${this.getNpmUrl(this.nextMajorVersion)}`);
}
try {
const publishedAt = await this.fetchPublishedTimestampForVersion(
this.nextMajorVersion,
);
if (publishedAt) {
this.cachedTimestamps[this.nextMajorVersion] = publishedAt;
}
} catch (e) {
if (process.env.DEBUG) {
console.error(`[debug] ${this.debugPrefix}: ${e}`);
}
}
// Refresh `lastFetched` regardless of whether or not the request to the npm
// registry was successful. Will try again after `majorVersionFetchIntervalDays`.
this.cachedTimestamps.lastFetched = new Date(this.now).toISOString();
await this.storage.set(this.cacheFile, this.cachedTimestamps);
}
/**
* Use previously cached data (local cache only, fast execution) to check
* whether this version of the CLI is deprecated as per deprecation policy,
* in which case warn about it and conditionally throw an error.
*/
public async warnAndAbortIfDeprecated() {
if (process.env.BALENARC_UNSUPPORTED) {
return; // for the benefit of code testing
}
await this.init();
const nextMajorDateStr = this.cachedTimestamps[this.nextMajorVersion];
if (!nextMajorDateStr) {
return;
}
const nextMajorDate = new Date(nextMajorDateStr).getTime();
const daysElapsed = Math.trunc((this.now - nextMajorDate) / this.msInDay);
if (daysElapsed > this.expiryDays) {
const { ExpectedError } = await import('./errors');
throw new ExpectedError(this.getExpiryMsg(daysElapsed));
} else if (daysElapsed > this.deprecationDays && process.stderr.isTTY) {
console.error(this.getDeprecationMsg(daysElapsed));
}
}
/** Separate function for the benefit of code testing */
getDeprecationMsg(daysElapsed: number) {
const { warnify } =
require('./utils/messages') as typeof import('./utils/messages');
return warnify(`\
CLI version ${this.nextMajorVersion} was released ${daysElapsed} days ago: please upgrade.
This version of the balena CLI (${this.currentVersion}) will exit with an error
message after ${this.expiryDays} days from the release of version ${this.nextMajorVersion},
as per deprecation policy: https://git.io/JRHUW#deprecation-policy
The --unsupported flag may be used to bypass this deprecation check and
allow the CLI to keep working beyond the deprecation period. However,
note that the balenaCloud or openBalena backends may be updated in a way
that is no longer compatible with this version.`);
}
/** Separate function the benefit of code testing */
getExpiryMsg(daysElapsed: number) {
return `
This version of the balena CLI (${this.currentVersion}) has expired: please upgrade.
${daysElapsed} days have passed since the release of CLI version ${this.nextMajorVersion}.
See deprecation policy at: https://git.io/JRHUW#deprecation-policy
The --unsupported flag may be used to bypass this deprecation check and
continue using this version of the CLI. However, note that the balenaCloud
or openBalena backends may be updated in a way that is no longer compatible
with this CLI version.`;
}
}

View File

@ -286,24 +286,3 @@ export const printErrorMessage = function (message: string) {
export const printExpectedErrorMessage = function (message: string) { export const printExpectedErrorMessage = function (message: string) {
console.error(`${message}\n`); console.error(`${message}\n`);
}; };
/**
* Print a friendly error message and exit the CLI with an error code, BYPASSING
* error reporting through Sentry.io's platform (raven.Raven.captureException).
* Note that lib/errors.ts provides top-level error handling code to catch any
* otherwise uncaught errors, AND to report them through Sentry.io. But many
* "expected" errors (say, a JSON parsing error in a file provided by the user)
* don't warrant reporting through Sentry.io. For such mundane errors, catch
* them and call this function.
*
* DEPRECATED: Use `throw new ExpectedError(<message>)` instead.
* If a specific process exit code x must be set, use process.exitCode = x
*/
export function exitWithExpectedError(message: string | Error): never {
if (message instanceof Error) {
({ message } = message);
}
printErrorMessage(message);
process.exit(1);
}

View File

@ -14,27 +14,17 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
import * as _ from 'lodash';
import * as Mixpanel from 'mixpanel';
import * as packageJSON from '../package.json'; import * as packageJSON from '../package.json';
import { getBalenaSdk } from './utils/lazy'; import { getBalenaSdk } from './utils/lazy';
const getMixpanel = _.once((balenaUrl: string) => {
return Mixpanel.init('balena-main', {
host: `api.${balenaUrl}`,
path: '/mixpanel',
protocol: 'https',
});
});
interface CachedUsername { interface CachedUsername {
token: string; token: string;
username: string; username: string;
} }
/** /**
* Mixpanel.com analytics tracking (information on balena CLI usage). * Track balena CLI usage events (product improvement analytics).
* *
* @param commandSignature A string like, for example: * @param commandSignature A string like, for example:
* "push <fleetOrDevice>" * "push <fleetOrDevice>"
@ -60,11 +50,10 @@ export async function trackCommand(commandSignature: string) {
}); });
} }
const settings = await import('balena-settings-client'); const settings = await import('balena-settings-client');
const balenaUrl = settings.get('balenaUrl') as string;
const username = await (async () => { const username = await (async () => {
const getStorage = await import('balena-settings-storage'); const getStorage = await import('balena-settings-storage');
const dataDirectory = settings.get('dataDirectory') as string; const dataDirectory = settings.get<string>('dataDirectory');
const storage = getStorage({ dataDirectory }); const storage = getStorage({ dataDirectory });
let token; let token;
try { try {
@ -94,8 +83,6 @@ export async function trackCommand(commandSignature: string) {
} }
})(); })();
const mixpanel = getMixpanel(balenaUrl);
if (!process.env.BALENARC_NO_SENTRY) { if (!process.env.BALENARC_NO_SENTRY) {
Sentry!.configureScope((scope) => { Sentry!.configureScope((scope) => {
scope.setUser({ scope.setUser({
@ -109,16 +96,43 @@ export async function trackCommand(commandSignature: string) {
!process.env.BALENA_CLI_TEST_TYPE && !process.env.BALENA_CLI_TEST_TYPE &&
!process.env.BALENARC_NO_ANALYTICS !process.env.BALENARC_NO_ANALYTICS
) { ) {
await mixpanel.track(`[CLI] ${commandSignature}`, { const balenaUrl = settings.get<string>('balenaUrl');
distinct_id: username, await sendEvent(balenaUrl, `[CLI] ${commandSignature}`, username);
version: packageJSON.version,
node: process.version,
arch: process.arch,
balenaUrl, // e.g. 'balena-cloud.com' or 'balena-staging.com'
platform: process.platform,
});
} }
} catch { } catch {
// ignore // ignore
} }
} }
/**
* Make the event tracking HTTPS request to balenaCloud's '/mixpanel' endpoint.
*/
async function sendEvent(balenaUrl: string, event: string, username?: string) {
const { default: got } = await import('got');
const trackData = {
event,
properties: {
arch: process.arch,
balenaUrl, // e.g. 'balena-cloud.com' or 'balena-staging.com'
distinct_id: username,
mp_lib: 'node',
node: process.version,
platform: process.platform,
token: 'balena-main',
version: packageJSON.version,
},
};
const url = `https://api.${balenaUrl}/mixpanel/track`;
const searchParams = {
ip: 0,
verbose: 0,
data: Buffer.from(JSON.stringify(trackData)).toString('base64'),
};
try {
await got(url, { searchParams, retry: 0 });
} catch (e) {
if (process.env.DEBUG) {
console.error(`[debug] Event tracking error: ${e.message || e}`);
}
}
}

View File

@ -46,7 +46,7 @@ export default class BalenaHelp extends Help {
const subject = getHelpSubject(argv); const subject = getHelpSubject(argv);
if (!subject) { if (!subject) {
const verbose = argv.includes('-v') || argv.includes('--verbose'); const verbose = argv.includes('-v') || argv.includes('--verbose');
this.showCustomRootHelp(verbose); console.log(this.getCustomRootHelp(verbose));
return; return;
} }
@ -80,67 +80,106 @@ export default class BalenaHelp extends Help {
throw new ExpectedError(`command ${chalk.cyan.bold(subject)} not found`); throw new ExpectedError(`command ${chalk.cyan.bold(subject)} not found`);
} }
showCustomRootHelp(showAllCommands: boolean): void { getCustomRootHelp(showAllCommands: boolean): string {
const chalk = getChalk(); const { bold, cyan } = getChalk();
const bold = chalk.bold;
const cmd = chalk.cyan.bold;
let commands = this.config.commands; let commands = this.config.commands;
commands = commands.filter((c) => this.opts.all || !c.hidden); commands = commands.filter((c) => this.opts.all || !c.hidden);
// Get Primary Commands, sorted as in manual list // Get Primary Commands, sorted as in manual list
const primaryCommands = this.manuallySortedPrimaryCommands.map((pc) => { const primaryCommands = this.manuallySortedPrimaryCommands
return commands.find((c) => c.id === pc.replace(' ', ':')); .map((pc) => {
}); return commands.find((c) => c.id === pc.replace(' ', ':'));
})
.filter((c): c is typeof commands[0] => !!c);
// Get the rest as Additional Commands let usageLength = 0;
const additionalCommands = commands.filter( for (const cmd of primaryCommands) {
(c) => usageLength = Math.max(usageLength, cmd.usage?.length || 0);
!this.manuallySortedPrimaryCommands.includes(c.id.replace(':', ' ')),
);
// Find longest usage, and pad usage of first command in each category
// This is to ensure that both categories align visually
const usageLength = commands
.map((c) => c.usage?.length || 0)
.reduce((longest, l) => {
return l > longest ? l : longest;
});
if (
typeof primaryCommands[0]?.usage === 'string' &&
typeof additionalCommands[0]?.usage === 'string'
) {
primaryCommands[0].usage = primaryCommands[0].usage.padEnd(usageLength);
additionalCommands[0].usage =
additionalCommands[0].usage.padEnd(usageLength);
} }
// Output help let additionalCmdSection: string[];
console.log(bold('USAGE'));
console.log('$ balena [COMMAND] [OPTIONS]');
console.log(bold('\nPRIMARY COMMANDS'));
console.log(this.formatCommands(primaryCommands));
if (showAllCommands) { if (showAllCommands) {
console.log(bold('\nADDITIONAL COMMANDS')); // Get the rest as Additional Commands
console.log(this.formatCommands(additionalCommands)); const additionalCommands = commands.filter(
(c) =>
!this.manuallySortedPrimaryCommands.includes(c.id.replace(':', ' ')),
);
// Find longest usage, and pad usage of first command in each category
// This is to ensure that both categories align visually
for (const cmd of additionalCommands) {
usageLength = Math.max(usageLength, cmd.usage?.length || 0);
}
if (
typeof primaryCommands[0].usage === 'string' &&
typeof additionalCommands[0].usage === 'string'
) {
primaryCommands[0].usage = primaryCommands[0].usage.padEnd(usageLength);
additionalCommands[0].usage =
additionalCommands[0].usage.padEnd(usageLength);
}
additionalCmdSection = [
bold('\nADDITIONAL COMMANDS'),
this.formatCommands(additionalCommands),
];
} else { } else {
console.log( const cmd = cyan.bold('balena help --verbose');
`\n${bold('...MORE')} run ${cmd( additionalCmdSection = [
'balena help --verbose', `\n${bold('...MORE')} run ${cmd} to list additional commands.`,
)} to list additional commands.`, ];
}
const globalOps = [
['--help, -h', 'display command help'],
['--debug', 'enable debug output'],
[
'--unsupported',
`\
prevent exit with an error as per Deprecation Policy
See: https://git.io/JRHUW#deprecation-policy`,
],
];
globalOps[0][0] = globalOps[0][0].padEnd(usageLength);
const { deprecationPolicyNote, reachingOut } =
require('./utils/messages') as typeof import('./utils/messages');
return [
bold('USAGE'),
'$ balena [COMMAND] [OPTIONS]',
bold('\nPRIMARY COMMANDS'),
this.formatCommands(primaryCommands),
...additionalCmdSection,
bold('\nGLOBAL OPTIONS'),
this.formatGlobalOpts(globalOps),
bold('\nDeprecation Policy Reminder'),
deprecationPolicyNote,
reachingOut,
].join('\n');
}
protected formatGlobalOpts(opts: string[][]) {
const { dim } = getChalk();
const outLines: string[] = [];
let flagWidth = 0;
for (const opt of opts) {
flagWidth = Math.max(flagWidth, opt[0].length);
}
for (const opt of opts) {
const descriptionLines = opt[1].split('\n');
outLines.push(
` ${opt[0].padEnd(flagWidth + 2)}${dim(descriptionLines[0])}`,
);
outLines.push(
...descriptionLines
.slice(1)
.map((line) => ` ${' '.repeat(flagWidth + 2)}${dim(line)}`),
); );
} }
return outLines.join('\n');
console.log(bold('\nGLOBAL OPTIONS'));
console.log(' --help, -h');
console.log(' --debug\n');
const { reachingOut } =
require('./utils/messages') as typeof import('./utils/messages');
console.log(reachingOut);
} }
protected formatCommands(commands: any[]): string { protected formatCommands(commands: any[]): string {

View File

@ -28,7 +28,7 @@ export const trackPromise = new Promise((resolve) => {
* parsed by oclif, but before the command's run() function is called. * parsed by oclif, but before the command's run() function is called.
* See: https://oclif.io/docs/hooks * See: https://oclif.io/docs/hooks
* *
* This hook is used to track CLI command signatures with mixpanel. * This hook is used to track CLI command signatures (usage analytics).
* A command signature is something like "env add NAME [VALUE]". That's * A command signature is something like "env add NAME [VALUE]". That's
* literally so: 'NAME' and 'VALUE' are NOT replaced with actual values. * literally so: 'NAME' and 'VALUE' are NOT replaced with actual values.
*/ */

View File

@ -14,8 +14,8 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
import { stripIndent } from './utils/lazy';
import { exitWithExpectedError } from './errors'; export let unsupportedFlag = false;
export interface AppOptions { export interface AppOptions {
// Prevent the default behavior of flushing stdout after running a command // Prevent the default behavior of flushing stdout after running a command
@ -50,11 +50,10 @@ export async function preparseArgs(argv: string[]): Promise<string[]> {
} }
// support global --debug flag // support global --debug flag
const debugIndex = cmdSlice.indexOf('--debug'); if (extractBooleanFlag(cmdSlice, '--debug')) {
if (debugIndex > -1) {
process.env.DEBUG = '1'; process.env.DEBUG = '1';
cmdSlice.splice(debugIndex, 1);
} }
unsupportedFlag = extractBooleanFlag(cmdSlice, '--unsupported');
} }
// Enable bluebird long stack traces when in debug mode, must be set // Enable bluebird long stack traces when in debug mode, must be set
@ -87,11 +86,22 @@ export async function preparseArgs(argv: string[]): Promise<string[]> {
return args; return args;
} }
function extractBooleanFlag(argv: string[], flag: string): boolean {
const index = argv.indexOf(flag);
if (index >= 0) {
argv.splice(index, 1);
return true;
}
return false;
}
/** /**
* Check whether the command line refers to a command that has been deprecated * Check whether the command line refers to a command that has been deprecated
* and removed and, if so, exit with an informative error message. * and removed and, if so, exit with an informative error message.
*/ */
export function checkDeletedCommand(argvSlice: string[]): void { export function checkDeletedCommand(argvSlice: string[]): void {
const { ExpectedError } = require('./errors') as typeof import('./errors');
if (argvSlice[0] === 'help') { if (argvSlice[0] === 'help') {
argvSlice = argvSlice.slice(1); argvSlice = argvSlice.slice(1);
} }
@ -101,17 +111,16 @@ export function checkDeletedCommand(argvSlice: string[]): void {
version: string, version: string,
verb = 'replaced', verb = 'replaced',
) { ) {
exitWithExpectedError(stripIndent` throw new ExpectedError(`\
Note: the command "balena ${oldCmd}" was ${verb} in CLI version ${version}. Note: the command "balena ${oldCmd}" was ${verb} in CLI version ${version}.
Please use "balena ${alternative}" instead. Please use "balena ${alternative}" instead.`);
`);
} }
function removed(oldCmd: string, alternative: string, version: string) { function removed(oldCmd: string, alternative: string, version: string) {
let msg = `Note: the command "balena ${oldCmd}" was removed in CLI version ${version}.`; let msg = `Note: the command "balena ${oldCmd}" was removed in CLI version ${version}.`;
if (alternative) { if (alternative) {
msg = [msg, alternative].join('\n'); msg = [msg, alternative].join('\n');
} }
exitWithExpectedError(msg); throw new ExpectedError(msg);
} }
const stopAlternative = const stopAlternative =
'Please use "balena ssh -s" to access the host OS, then use `balena-engine stop`.'; 'Please use "balena ssh -s" to access the host OS, then use `balena-engine stop`.';

View File

@ -139,6 +139,14 @@ export async function downloadOSImage(
const displayVersion = OSVersion === 'default' ? '' : ` ${OSVersion}`; const displayVersion = OSVersion === 'default' ? '' : ` ${OSVersion}`;
// Override the default zlib flush value as we've seen cases of
// incomplete files being identified as successful downloads when using Z_SYNC_FLUSH.
// Using Z_NO_FLUSH results in a Z_BUF_ERROR instead of a corrupt image file.
// https://github.com/nodejs/node/blob/master/doc/api/zlib.md#zlib-constants
// Hopefully this is a temporary workaround until we can resolve
// some ongoing issues with the os download stream.
process.env.ZLIB_FLUSH = 'Z_NO_FLUSH';
const manager = await import('balena-image-manager'); const manager = await import('balena-image-manager');
const stream = await manager.get(deviceType, OSVersion); const stream = await manager.get(deviceType, OSVersion);
@ -188,18 +196,21 @@ async function resolveOSVersion(deviceType: string, version: string) {
return version; return version;
} }
const { versions: vs, recommended } = const vs = (
await getBalenaSdk().models.os.getSupportedVersions(deviceType); (await getBalenaSdk().models.hostapp.getAllOsVersions([deviceType]))[
deviceType
] ?? []
).filter((v) => v.osType === 'default');
const choices = vs.map((v) => ({ const choices = vs.map((v) => ({
value: v, value: v.rawVersion,
name: `v${v}` + (v === recommended ? ' (recommended)' : ''), name: `v${v.rawVersion}` + (v.isRecommended ? ' (recommended)' : ''),
})); }));
return getCliForm().ask({ return getCliForm().ask({
message: 'Select the OS version:', message: 'Select the OS version:',
type: 'list', type: 'list',
choices, choices,
default: recommended, default: (vs.find((v) => v.isRecommended) ?? vs[0])?.rawVersion,
}); });
} }

View File

@ -51,7 +51,7 @@ export interface ComposeOpts {
dockerfilePath?: string; dockerfilePath?: string;
inlineLogs?: boolean; inlineLogs?: boolean;
multiDockerignore: boolean; multiDockerignore: boolean;
nogitignore: boolean; nogitignore: boolean; // v13: delete this line
noParentCheck: boolean; noParentCheck: boolean;
projectName: string; projectName: string;
projectPath: string; projectPath: string;
@ -63,9 +63,9 @@ export interface ComposeCliFlags {
dockerfile?: string; dockerfile?: string;
logs: boolean; logs: boolean;
nologs: boolean; nologs: boolean;
gitignore: boolean; gitignore?: boolean; // v13: delete this line
nogitignore?: boolean; // v13: delete this line
'multi-dockerignore': boolean; 'multi-dockerignore': boolean;
nogitignore: boolean;
'noparent-check': boolean; 'noparent-check': boolean;
'registry-secrets'?: RegistrySecrets; 'registry-secrets'?: RegistrySecrets;
'convert-eol': boolean; 'convert-eol': boolean;
@ -89,6 +89,9 @@ export interface Release {
| 'commit' | 'commit'
| 'composition' | 'composition'
| 'source' | 'source'
| 'is_final'
| 'contract'
| 'semver'
| 'start_timestamp' | 'start_timestamp'
| 'end_timestamp' | 'end_timestamp'
>; >;
@ -99,6 +102,6 @@ interface TarDirectoryOptions {
composition?: Composition; composition?: Composition;
convertEol?: boolean; convertEol?: boolean;
multiDockerignore?: boolean; multiDockerignore?: boolean;
nogitignore: boolean; nogitignore: boolean; // v13: delete this line
preFinalizeCallback?: (pack: Pack) => void | Promise<void>; preFinalizeCallback?: (pack: Pack) => void | Promise<void>;
} }

View File

@ -18,6 +18,7 @@
import * as path from 'path'; import * as path from 'path';
import { ExpectedError } from '../errors'; import { ExpectedError } from '../errors';
import { getChalk } from './lazy'; import { getChalk } from './lazy';
import { isV13 } from './version';
/** /**
* @returns Promise<{import('./compose-types').ComposeOpts}> * @returns Promise<{import('./compose-types').ComposeOpts}>
@ -25,7 +26,7 @@ import { getChalk } from './lazy';
export function generateOpts(options) { export function generateOpts(options) {
const { promises: fs } = require('fs'); const { promises: fs } = require('fs');
if (options.gitignore && options['multi-dockerignore']) { if (!isV13() && options.gitignore && options['multi-dockerignore']) {
throw new ExpectedError( throw new ExpectedError(
'The --gitignore and --multi-dockerignore options cannot be used together', 'The --gitignore and --multi-dockerignore options cannot be used together',
); );
@ -37,7 +38,7 @@ export function generateOpts(options) {
convertEol: !options['noconvert-eol'], convertEol: !options['noconvert-eol'],
dockerfilePath: options.dockerfile, dockerfilePath: options.dockerfile,
multiDockerignore: !!options['multi-dockerignore'], multiDockerignore: !!options['multi-dockerignore'],
nogitignore: !options.gitignore, nogitignore: !options.gitignore, // v13: delete this line
noParentCheck: options['noparent-check'], noParentCheck: options['noparent-check'],
})); }));
} }
@ -48,10 +49,16 @@ export function generateOpts(options) {
/** /**
* @param {string} composePath * @param {string} composePath
* @param {string} composeStr * @param {string} composeStr
* @param {string | null} projectName * @param {string | undefined} projectName The --projectName flag (build, deploy)
* @param {string | undefined} imageTag The --tag flag (build, deploy)
* @returns {import('./compose-types').ComposeProject} * @returns {import('./compose-types').ComposeProject}
*/ */
export function createProject(composePath, composeStr, projectName = null) { export function createProject(
composePath,
composeStr,
projectName = '',
imageTag = '',
) {
const yml = require('js-yaml'); const yml = require('js-yaml');
const compose = require('resin-compose-parse'); const compose = require('resin-compose-parse');
@ -61,7 +68,7 @@ export function createProject(composePath, composeStr, projectName = null) {
}); });
const composition = compose.normalize(rawComposition); const composition = compose.normalize(rawComposition);
projectName ??= path.basename(composePath); projectName ||= path.basename(composePath);
const descriptors = compose.parse(composition).map(function (descr) { const descriptors = compose.parse(composition).map(function (descr) {
// generate an image name based on the project and service names // generate an image name based on the project and service names
@ -71,9 +78,8 @@ export function createProject(composePath, composeStr, projectName = null) {
descr.image.context != null && descr.image.context != null &&
descr.image.tag == null descr.image.tag == null
) { ) {
descr.image.tag = [projectName, descr.serviceName] const { makeImageName } = require('./compose_ts');
.join('_') descr.image.tag = makeImageName(projectName, descr.serviceName, imageTag);
.toLowerCase();
} }
return descr; return descr;
}); });
@ -92,6 +98,8 @@ export function createProject(composePath, composeStr, projectName = null) {
* @param {string} dir Source directory * @param {string} dir Source directory
* @param {import('./compose-types').TarDirectoryOptions} param * @param {import('./compose-types').TarDirectoryOptions} param
* @returns {Promise<import('stream').Readable>} * @returns {Promise<import('stream').Readable>}
*
* v13: delete this function
*/ */
export async function originalTarDirectory(dir, param) { export async function originalTarDirectory(dir, param) {
let { let {
@ -175,6 +183,9 @@ export async function originalTarDirectory(dir, param) {
* @param {number} userId * @param {number} userId
* @param {number} appId * @param {number} appId
* @param {import('resin-compose-parse').Composition} composition * @param {import('resin-compose-parse').Composition} composition
* @param {boolean} draft
* @param {string|undefined} semver
* @param {string|undefined} contract
* @returns {Promise<import('./compose-types').Release>} * @returns {Promise<import('./compose-types').Release>}
*/ */
export const createRelease = async function ( export const createRelease = async function (
@ -183,6 +194,9 @@ export const createRelease = async function (
userId, userId,
appId, appId,
composition, composition,
draft,
semver,
contract,
) { ) {
const _ = require('lodash'); const _ = require('lodash');
const crypto = require('crypto'); const crypto = require('crypto');
@ -197,6 +211,9 @@ export const createRelease = async function (
composition, composition,
source: 'local', source: 'local',
commit: crypto.pseudoRandomBytes(16).toString('hex').toLowerCase(), commit: crypto.pseudoRandomBytes(16).toString('hex').toLowerCase(),
semver,
is_final: !draft,
contract,
}); });
return { return {
@ -207,6 +224,9 @@ export const createRelease = async function (
'commit', 'commit',
'composition', 'composition',
'source', 'source',
'is_final',
'contract',
'semver',
'start_timestamp', 'start_timestamp',
'end_timestamp', 'end_timestamp',
]), ]),
@ -435,7 +455,6 @@ var pushProgressRenderer = function (tty, prefix) {
export class BuildProgressUI { export class BuildProgressUI {
constructor(tty, descriptors) { constructor(tty, descriptors) {
this._handleEvent = this._handleEvent.bind(this); this._handleEvent = this._handleEvent.bind(this);
this._handleInterrupt = this._handleInterrupt.bind(this);
this.start = this.start.bind(this); this.start = this.start.bind(this);
this.end = this.end.bind(this); this.end = this.end.bind(this);
this._display = this._display.bind(this); this._display = this._display.bind(this);
@ -487,14 +506,7 @@ export class BuildProgressUI {
this._serviceToDataMap[service] = event; this._serviceToDataMap[service] = event;
} }
_handleInterrupt() {
this._cancelled = true;
this.end();
return process.exit(130); // 128 + SIGINT
}
start() { start() {
process.on('SIGINT', this._handleInterrupt);
this._tty.hideCursor(); this._tty.hideCursor();
this._services.forEach((service) => { this._services.forEach((service) => {
this.streams[service].write({ status: 'Preparing...' }); this.streams[service].write({ status: 'Preparing...' });
@ -508,7 +520,6 @@ export class BuildProgressUI {
return; return;
} }
this._ended = true; this._ended = true;
process.removeListener('SIGINT', this._handleInterrupt);
this._runloop?.end(); this._runloop?.end();
this._runloop = null; this._runloop = null;

View File

@ -18,8 +18,9 @@ import { flags } from '@oclif/command';
import { BalenaSDK } from 'balena-sdk'; import { BalenaSDK } from 'balena-sdk';
import type { TransposeOptions } from 'docker-qemu-transpose'; import type { TransposeOptions } from 'docker-qemu-transpose';
import type * as Dockerode from 'dockerode'; import type * as Dockerode from 'dockerode';
import * as _ from 'lodash';
import { promises as fs } from 'fs'; import { promises as fs } from 'fs';
import jsyaml = require('js-yaml');
import * as _ from 'lodash';
import * as path from 'path'; import * as path from 'path';
import type { import type {
BuildConfig, BuildConfig,
@ -42,8 +43,11 @@ import {
import type { DeviceInfo } from './device/api'; import type { DeviceInfo } from './device/api';
import { getBalenaSdk, getChalk, stripIndent } from './lazy'; import { getBalenaSdk, getChalk, stripIndent } from './lazy';
import Logger = require('./logger'); import Logger = require('./logger');
import { isV13 } from './version';
import { exists } from './which'; import { exists } from './which';
const allowedContractTypes = ['sw.application', 'sw.block'];
/** /**
* Given an array representing the raw `--release-tag` flag of the deploy and * Given an array representing the raw `--release-tag` flag of the deploy and
* push commands, parse it into separate arrays of release tag keys and values. * push commands, parse it into separate arrays of release tag keys and values.
@ -114,6 +118,7 @@ export async function loadProject(
logger: Logger, logger: Logger,
opts: ComposeOpts, opts: ComposeOpts,
image?: string, image?: string,
imageTag?: string,
): Promise<ComposeProject> { ): Promise<ComposeProject> {
const compose = await import('resin-compose-parse'); const compose = await import('resin-compose-parse');
const { createProject } = await import('./compose'); const { createProject } = await import('./compose');
@ -152,7 +157,12 @@ export async function loadProject(
} }
} }
logger.logDebug('Creating project...'); logger.logDebug('Creating project...');
return createProject(opts.projectPath, composeStr, opts.projectName); return createProject(
opts.projectPath,
composeStr,
opts.projectName,
imageTag,
);
} }
/** /**
@ -234,7 +244,7 @@ interface Renderer {
streams: Dictionary<NodeJS.ReadWriteStream>; streams: Dictionary<NodeJS.ReadWriteStream>;
} }
export async function buildProject(opts: { export interface BuildProjectOpts {
docker: Dockerode; docker: Dockerode;
logger: Logger; logger: Logger;
projectPath: string; projectPath: string;
@ -247,84 +257,101 @@ export async function buildProject(opts: {
inlineLogs?: boolean; inlineLogs?: boolean;
convertEol: boolean; convertEol: boolean;
dockerfilePath?: string; dockerfilePath?: string;
nogitignore: boolean; nogitignore: boolean; // v13: delete this line
multiDockerignore: boolean; multiDockerignore: boolean;
}): Promise<BuiltImage[]> { }
const { logger, projectName } = opts;
logger.logInfo(`Building for ${opts.arch}/${opts.deviceType}`);
let buildSummaryByService: Dictionary<string> | undefined; export async function buildProject(
opts: BuildProjectOpts,
): Promise<BuiltImage[]> {
await checkBuildSecretsRequirements(opts.docker, opts.projectPath);
const compose = await import('resin-compose-parse'); const compose = await import('resin-compose-parse');
const imageDescriptors = compose.parse(opts.composition); const imageDescriptors = compose.parse(opts.composition);
const imageDescriptorsByServiceName = _.keyBy(
imageDescriptors,
'serviceName',
);
const renderer = await startRenderer({ imageDescriptors, ...opts }); const renderer = await startRenderer({ imageDescriptors, ...opts });
let buildSummaryByService: Dictionary<string> | undefined;
try { try {
await checkBuildSecretsRequirements(opts.docker, opts.projectPath); const { awaitInterruptibleTask } = await import('./helpers');
const [images, summaryMsgByService] = await awaitInterruptibleTask(
const needsQemu = await installQemuIfNeeded({ ...opts, imageDescriptors }); $buildProject,
imageDescriptors,
const tarStream = await tarDirectory(opts.projectPath, opts); renderer,
const tasks: BuildTaskPlus[] = await makeBuildTasks(
opts.composition,
tarStream,
opts, opts,
logger,
projectName,
); );
setTaskAttributes({ tasks, imageDescriptorsByServiceName, ...opts });
const transposeOptArray: Array<TransposeOptions | undefined> =
await Promise.all(
tasks.map((task) => {
// Setup emulation if needed
if (needsQemu && !task.external) {
return qemuTransposeBuildStream({ task, ...opts });
}
}),
);
await Promise.all(
// transposeOptions may be undefined. That's OK.
transposeOptArray.map((transposeOptions, index) =>
setTaskProgressHooks({
task: tasks[index],
renderer,
transposeOptions,
...opts,
}),
),
);
logger.logDebug('Prepared tasks; building...');
const { BALENA_ENGINE_TMP_PATH } = await import('../config');
const builder = await import('resin-multibuild');
const builtImages = await builder.performBuilds(
tasks,
opts.docker,
BALENA_ENGINE_TMP_PATH,
);
const [images, summaryMsgByService] = await inspectBuiltImages({
builtImages,
imageDescriptorsByServiceName,
tasks,
...opts,
});
buildSummaryByService = summaryMsgByService; buildSummaryByService = summaryMsgByService;
return images; return images;
} finally { } finally {
renderer.end(buildSummaryByService); renderer.end(buildSummaryByService);
} }
} }
async function $buildProject(
imageDescriptors: ImageDescriptor[],
renderer: Renderer,
opts: BuildProjectOpts,
): Promise<[BuiltImage[], Dictionary<string>]> {
const { logger, projectName } = opts;
logger.logInfo(`Building for ${opts.arch}/${opts.deviceType}`);
const needsQemu = await installQemuIfNeeded({ ...opts, imageDescriptors });
const tarStream = await tarDirectory(opts.projectPath, opts);
const tasks: BuildTaskPlus[] = await makeBuildTasks(
opts.composition,
tarStream,
opts,
logger,
projectName,
);
const imageDescriptorsByServiceName = _.keyBy(
imageDescriptors,
'serviceName',
);
setTaskAttributes({ tasks, imageDescriptorsByServiceName, ...opts });
const transposeOptArray: Array<TransposeOptions | undefined> =
await Promise.all(
tasks.map((task) => {
// Setup emulation if needed
if (needsQemu && !task.external) {
return qemuTransposeBuildStream({ task, ...opts });
}
}),
);
await Promise.all(
// transposeOptions may be undefined. That's OK.
transposeOptArray.map((transposeOptions, index) =>
setTaskProgressHooks({
task: tasks[index],
renderer,
transposeOptions,
...opts,
}),
),
);
logger.logDebug('Prepared tasks; building...');
const { BALENA_ENGINE_TMP_PATH } = await import('../config');
const builder = await import('resin-multibuild');
const builtImages = await builder.performBuilds(
tasks,
opts.docker,
BALENA_ENGINE_TMP_PATH,
);
return await inspectBuiltImages({
builtImages,
imageDescriptorsByServiceName,
tasks,
...opts,
});
}
async function startRenderer({ async function startRenderer({
imageDescriptors, imageDescriptors,
inlineLogs, inlineLogs,
@ -390,6 +417,18 @@ async function installQemuIfNeeded({
return needsQemu; return needsQemu;
} }
export function makeImageName(
projectName: string,
serviceName: string,
tag?: string,
) {
let name = `${projectName}_${serviceName}`;
if (tag) {
name = [name, tag].map((s) => s.replace(/:/g, '_')).join(':');
}
return name.toLowerCase();
}
function setTaskAttributes({ function setTaskAttributes({
tasks, tasks,
buildOpts, buildOpts,
@ -405,7 +444,7 @@ function setTaskAttributes({
const d = imageDescriptorsByServiceName[task.serviceName]; const d = imageDescriptorsByServiceName[task.serviceName];
// multibuild (splitBuildStream) parses the composition internally so // multibuild (splitBuildStream) parses the composition internally so
// any tags we've set before are lost; re-assign them here // any tags we've set before are lost; re-assign them here
task.tag ??= [projectName, task.serviceName].join('_').toLowerCase(); task.tag ??= makeImageName(projectName, task.serviceName, buildOpts.t);
if (isBuildConfig(d.image)) { if (isBuildConfig(d.image)) {
d.image.tag = task.tag; d.image.tag = task.tag;
} }
@ -686,7 +725,7 @@ export async function getServiceDirsFromComposition(
* Return true if `image` is actually a docker-compose.yml `services.service.build` * Return true if `image` is actually a docker-compose.yml `services.service.build`
* configuration object, rather than an "external image" (`services.service.image`). * configuration object, rather than an "external image" (`services.service.image`).
* *
* The `image` argument may therefore refere to either a `build` or `image` property * The `image` argument may therefore refer to either a `build` or `image` property
* of a service in a docker-compose.yml file, which is a bit confusing but it matches * of a service in a docker-compose.yml file, which is a bit confusing but it matches
* the `ImageDescriptor.image` property as defined by `resin-compose-parse`. * the `ImageDescriptor.image` property as defined by `resin-compose-parse`.
* *
@ -717,8 +756,8 @@ export async function tarDirectory(
dir: string, dir: string,
param: TarDirectoryOptions, param: TarDirectoryOptions,
): Promise<import('stream').Readable> { ): Promise<import('stream').Readable> {
const { nogitignore = false } = param; const { nogitignore = false } = param; // v13: delete this line
if (nogitignore) { if (isV13() || nogitignore) {
return newTarDirectory(dir, param); return newTarDirectory(dir, param);
} else { } else {
return (await import('./compose')).originalTarDirectory(dir, param); return (await import('./compose')).originalTarDirectory(dir, param);
@ -739,11 +778,13 @@ async function newTarDirectory(
composition, composition,
convertEol = false, convertEol = false,
multiDockerignore = false, multiDockerignore = false,
nogitignore = false, nogitignore = false, // v13: delete this line
preFinalizeCallback, preFinalizeCallback,
}: TarDirectoryOptions, }: TarDirectoryOptions,
): Promise<import('stream').Readable> { ): Promise<import('stream').Readable> {
require('assert').strict.equal(nogitignore, true); if (!isV13()) {
require('assert').strict.equal(nogitignore, true);
}
const { filterFilesWithDockerignore } = await import('./ignore'); const { filterFilesWithDockerignore } = await import('./ignore');
const { toPosixPath } = (await import('resin-multibuild')).PathUtils; const { toPosixPath } = (await import('resin-multibuild')).PathUtils;
@ -859,7 +900,8 @@ function printDockerignoreWarn(
} }
} }
if (msg.length) { if (msg.length) {
logFunc.call(logger, [' ', hr, ...msg, hr].join('\n')); const { warnify } = require('./messages') as typeof import('./messages');
logFunc.call(logger, ' \n' + warnify(msg.join('\n'), ''));
} }
} }
@ -868,11 +910,16 @@ function printDockerignoreWarn(
* found and the --gitignore (-g) option has been provided (v11 compatibility). * found and the --gitignore (-g) option has been provided (v11 compatibility).
* @param dockerignoreFile Absolute path to a .dockerignore file * @param dockerignoreFile Absolute path to a .dockerignore file
* @param gitignoreFiles Array of absolute paths to .gitginore files * @param gitignoreFiles Array of absolute paths to .gitginore files
*
* v13: delete this function
*/ */
export function printGitignoreWarn( export function printGitignoreWarn(
dockerignoreFile: string, dockerignoreFile: string,
gitignoreFiles: string[], gitignoreFiles: string[],
) { ) {
if (isV13()) {
return;
}
const ignoreFiles = [dockerignoreFile, ...gitignoreFiles].filter((e) => e); const ignoreFiles = [dockerignoreFile, ...gitignoreFiles].filter((e) => e);
if (ignoreFiles.length === 0) { if (ignoreFiles.length === 0) {
return; return;
@ -1004,9 +1051,7 @@ export async function makeBuildTasks(
infoStr = `build [${task.context}]`; infoStr = `build [${task.context}]`;
} }
logger.logDebug(` ${task.serviceName}: ${infoStr}`); logger.logDebug(` ${task.serviceName}: ${infoStr}`);
// Workaround for Docker v20.10 + single-arch base images. See: task.logger = logger.getAdapter();
// https://www.flowdock.com/app/rulemotion/i-cli/threads/RuSu1KiWOn62xaGy7O2sn8m8BUc
task.dockerPlatform = 'none';
}); });
logger.logDebug( logger.logDebug(
@ -1063,18 +1108,21 @@ async function performResolution(
if (!buildTask.buildStream) { if (!buildTask.buildStream) {
continue; continue;
} }
// Consume each task.buildStream in order to trigger the let error: Error | undefined;
// resolution events that define fields like: try {
// task.dockerfile, task.dockerfilePath, // Consume each task.buildStream in order to trigger the
// task.projectType, task.resolved // resolution events that define fields like:
// This mimics what is currently done in `resin-builder`. // task.dockerfile, task.dockerfilePath,
const clonedStream: Pack = await cloneTarStream( // task.projectType, task.resolved
buildTask.buildStream, // This mimics what is currently done in `resin-builder`.
); buildTask.buildStream = await cloneTarStream(buildTask.buildStream);
buildTask.buildStream = clonedStream; } catch (e) {
if (!buildTask.external && !buildTask.resolved) { error = e;
}
if (error || (!buildTask.external && !buildTask.resolved)) {
const cause = error ? `${error}\n` : '';
throw new ExpectedError( throw new ExpectedError(
`Project type for service "${buildTask.serviceName}" could not be determined. Missing a Dockerfile?`, `${cause}Project type for service "${buildTask.serviceName}" could not be determined. Missing a Dockerfile?`,
); );
} }
} }
@ -1285,6 +1333,9 @@ async function pushServiceImages(
); );
} }
// TODO: This should be shared between the CLI & the Builder
const PLAIN_SEMVER_REGEX = /^([0-9]+)\.([0-9]+)\.([0-9]+)$/;
export async function deployProject( export async function deployProject(
docker: import('dockerode'), docker: import('dockerode'),
logger: Logger, logger: Logger,
@ -1295,6 +1346,8 @@ export async function deployProject(
auth: string, auth: string,
apiEndpoint: string, apiEndpoint: string,
skipLogUpload: boolean, skipLogUpload: boolean,
projectPath: string,
isDraft: boolean,
): Promise<import('balena-release/build/models').ReleaseModel> { ): Promise<import('balena-release/build/models').ReleaseModel> {
const releaseMod = await import('balena-release'); const releaseMod = await import('balena-release');
const { createRelease, tagServiceImages } = await import('./compose'); const { createRelease, tagServiceImages } = await import('./compose');
@ -1303,11 +1356,29 @@ export async function deployProject(
const prefix = getChalk().cyan('[Info]') + ' '; const prefix = getChalk().cyan('[Info]') + ' ';
const spinner = createSpinner(); const spinner = createSpinner();
const contractPath = path.join(projectPath, 'balena.yml');
const contract = await getContractContent(contractPath);
if (contract?.version && !PLAIN_SEMVER_REGEX.test(contract?.version)) {
throw new ExpectedError(stripIndent`\
Error: expected the version field in "${contractPath}"
to be a basic semver in the format '1.2.3'. Got '${contract.version}' instead`);
}
const $release = await runSpinner( const $release = await runSpinner(
tty, tty,
spinner, spinner,
`${prefix}Creating release...`, `${prefix}Creating release...`,
() => createRelease(apiEndpoint, auth, userId, appId, composition), () =>
createRelease(
apiEndpoint,
auth,
userId,
appId,
composition,
isDraft,
contract?.version,
contract ? JSON.stringify(contract) : undefined,
),
); );
const { client: pineClient, release, serviceImages } = $release; const { client: pineClient, release, serviceImages } = $release;
@ -1315,20 +1386,25 @@ export async function deployProject(
logger.logDebug('Tagging images...'); logger.logDebug('Tagging images...');
const taggedImages = await tagServiceImages(docker, images, serviceImages); const taggedImages = await tagServiceImages(docker, images, serviceImages);
try { try {
const token = await getTokenForPreviousRepos( const { awaitInterruptibleTask } = await import('./helpers');
logger, // awaitInterruptibleTask throws SIGINTError on CTRL-C,
appId, // causing the release status to be set to 'failed'
apiEndpoint, await awaitInterruptibleTask(async () => {
taggedImages, const token = await getTokenForPreviousRepos(
); logger,
await pushServiceImages( appId,
docker, apiEndpoint,
logger, taggedImages,
pineClient, );
taggedImages, await pushServiceImages(
token, docker,
skipLogUpload, logger,
); pineClient,
taggedImages,
token,
skipLogUpload,
);
});
release.status = 'success'; release.status = 'success';
} catch (err) { } catch (err) {
release.status = 'failed'; release.status = 'failed';
@ -1392,6 +1468,42 @@ export function createRunLoop(tick: (...args: any[]) => void) {
return runloop; return runloop;
} }
async function getContractContent(
filePath: string,
): Promise<Dictionary<any> | undefined> {
let fileContentAsString;
try {
fileContentAsString = await fs.readFile(filePath, 'utf8');
} catch (e) {
if (e.code === 'ENOENT') {
return; // File does not exist
}
throw e;
}
let asJson;
try {
asJson = jsyaml.load(fileContentAsString);
} catch (err) {
throw new ExpectedError(
`Error parsing file "${filePath}":\n ${err.message}`,
);
}
if (!isContract(asJson)) {
throw new ExpectedError(
stripIndent`Error: application contract in '${filePath}' needs to
define a top level "type" field with an allowed application type.
Allowed application types are: ${allowedContractTypes.join(', ')}`,
);
}
return asJson;
}
function isContract(obj: any): obj is Dictionary<any> {
return obj?.type && allowedContractTypes.includes(obj.type);
}
function createLogStream(input: Readable) { function createLogStream(input: Readable) {
const split = require('split') as typeof import('split'); const split = require('split') as typeof import('split');
const stripAnsi = require('strip-ansi-stream'); const stripAnsi = require('strip-ansi-stream');
@ -1532,22 +1644,26 @@ export const composeCliFlags: flags.Input<ComposeCliFlags> = {
description: description:
'Hide the image build log output (produce less verbose output)', 'Hide the image build log output (produce less verbose output)',
}), }),
gitignore: flags.boolean({ ...(isV13()
description: stripIndent` ? {}
Consider .gitignore files in addition to the .dockerignore file. This reverts : {
to the CLI v11 behavior/implementation (deprecated) if compatibility is required gitignore: flags.boolean({
until your project can be adapted.`, description: stripIndent`
char: 'g', Consider .gitignore files in addition to the .dockerignore file. This reverts
}), to the CLI v11 behavior/implementation (deprecated) if compatibility is required
until your project can be adapted.`,
char: 'g',
}),
nogitignore: flags.boolean({
description: `No-op (default behavior) since balena CLI v12.0.0. See "balena help build".`,
char: 'G',
}),
}),
'multi-dockerignore': flags.boolean({ 'multi-dockerignore': flags.boolean({
description: description:
'Have each service use its own .dockerignore file. See "balena help build".', 'Have each service use its own .dockerignore file. See "balena help build".',
char: 'm', char: 'm',
}), }),
nogitignore: flags.boolean({
description: `No-op (default behavior) since balena CLI v12.0.0. See "balena help build".`,
char: 'G',
}),
'noparent-check': flags.boolean({ 'noparent-check': flags.boolean({
description: description:
"Disable project validation check of 'docker-compose.yml' file in parent folder", "Disable project validation check of 'docker-compose.yml' file in parent folder",
@ -1566,8 +1682,9 @@ export const composeCliFlags: flags.Input<ComposeCliFlags> = {
"Don't convert line endings from CRLF (Windows format) to LF (Unix format).", "Don't convert line endings from CRLF (Windows format) to LF (Unix format).",
}), }),
projectName: flags.string({ projectName: flags.string({
description: description: stripIndent`\
'Specify an alternate project name; default is the directory name', Name prefix for locally built images. This is the 'projectName' portion
in 'projectName_serviceName:tag'. The default is the directory name.`,
char: 'n', char: 'n',
}), }),
}; };

View File

@ -20,6 +20,7 @@ import * as os from 'os';
import * as request from 'request'; import * as request from 'request';
import type * as Stream from 'stream'; import type * as Stream from 'stream';
import { retry } from '../helpers';
import Logger = require('../logger'); import Logger = require('../logger');
import * as ApiErrors from './errors'; import * as ApiErrors from './errors';
@ -211,17 +212,21 @@ export class DeviceAPI {
); );
return; return;
} }
res.socket.setKeepAlive(true, 1000); try {
if (os.platform() !== 'win32') { res.socket.setKeepAlive(true, 1000);
const NetKeepalive = await import('net-keepalive'); if (os.platform() !== 'win32') {
// Certain versions of typescript won't convert const NetKeepalive = await import('net-keepalive');
// this automatically // Certain versions of typescript won't convert
const sock = res.socket as any as NodeJSSocketWithFileDescriptor; // this automatically
// We send a tcp keepalive probe once every 5 seconds const sock = res.socket as any as NodeJSSocketWithFileDescriptor;
NetKeepalive.setKeepAliveInterval(sock, 5000); // We send a tcp keepalive probe once every 5 seconds
// After 5 failed probes, the connection is marked as NetKeepalive.setKeepAliveInterval(sock, 5000);
// closed // After 5 failed probes, the connection is marked as
NetKeepalive.setKeepAliveProbes(sock, 5); // closed
NetKeepalive.setKeepAliveProbes(sock, 5);
}
} catch (error) {
reject(error);
} }
resolve(res); resolve(res);
}); });
@ -256,24 +261,35 @@ export class DeviceAPI {
} }
} }
return new Promise((resolve, reject) => { const doRequest = async () => {
return request(opts, (err, response, body) => { return await new Promise((resolve, reject) => {
if (err) { return request(opts, (err, response, body) => {
return reject(err); if (err) {
} return reject(err);
switch (response.statusCode) { }
case 200: switch (response.statusCode) {
return resolve(body); case 200:
case 400: return resolve(body);
return reject(new ApiErrors.BadRequestDeviceAPIError(body.message)); case 400:
case 503: return reject(
return reject( new ApiErrors.BadRequestDeviceAPIError(body.message),
new ApiErrors.ServiceUnavailableAPIError(body.message), );
); case 503:
default: return reject(
return reject(new ApiErrors.DeviceAPIError(body.message)); new ApiErrors.ServiceUnavailableAPIError(body.message),
} );
default:
return reject(new ApiErrors.DeviceAPIError(body.message));
}
});
}); });
};
return await retry({
func: doRequest,
initialDelayMs: 2000,
maxAttempts: 6,
label: `Supervisor API (${opts.method} ${(opts as any).url})`,
}); });
} }
} }

View File

@ -57,7 +57,7 @@ export interface DeviceDeployOptions {
registrySecrets: RegistrySecrets; registrySecrets: RegistrySecrets;
multiDockerignore: boolean; multiDockerignore: boolean;
nocache: boolean; nocache: boolean;
nogitignore: boolean; nogitignore: boolean; // v13: delete this line
noParentCheck: boolean; noParentCheck: boolean;
nolive: boolean; nolive: boolean;
pull: boolean; pull: boolean;
@ -182,7 +182,7 @@ export async function deployToDevice(opts: DeviceDeployOptions): Promise<void> {
convertEol: opts.convertEol, convertEol: opts.convertEol,
dockerfilePath: opts.dockerfilePath, dockerfilePath: opts.dockerfilePath,
multiDockerignore: opts.multiDockerignore, multiDockerignore: opts.multiDockerignore,
nogitignore: opts.nogitignore, nogitignore: opts.nogitignore, // v13: delete this line
noParentCheck: opts.noParentCheck, noParentCheck: opts.noParentCheck,
projectName: 'local', projectName: 'local',
projectPath: opts.source, projectPath: opts.source,
@ -201,7 +201,7 @@ export async function deployToDevice(opts: DeviceDeployOptions): Promise<void> {
composition: project.composition, composition: project.composition,
convertEol: opts.convertEol, convertEol: opts.convertEol,
multiDockerignore: opts.multiDockerignore, multiDockerignore: opts.multiDockerignore,
nogitignore: opts.nogitignore, nogitignore: opts.nogitignore, // v13: delete this line
}); });
// Try to detect the device information // Try to detect the device information
@ -310,7 +310,7 @@ function connectToDocker(host: string, port: number): Docker {
}); });
} }
export async function performBuilds( async function performBuilds(
composition: Composition, composition: Composition,
tarStream: Readable, tarStream: Readable,
docker: Docker, docker: Docker,
@ -426,7 +426,7 @@ export async function rebuildSingleTask(
composition, composition,
convertEol: opts.convertEol, convertEol: opts.convertEol,
multiDockerignore: opts.multiDockerignore, multiDockerignore: opts.multiDockerignore,
nogitignore: opts.nogitignore, nogitignore: opts.nogitignore, // v13: delete this line
}); });
const task = _.find( const task = _.find(

View File

@ -80,16 +80,19 @@ async function displayDeviceLogs(
jsonStream.on('error', (e) => { jsonStream.on('error', (e) => {
logger.logWarn(`Error parsing NDJSON log chunk: ${e}`); logger.logWarn(`Error parsing NDJSON log chunk: ${e}`);
}); });
logs.once('error', reject); logs.once('error', handleError);
logs.once('end', () => { logs.once('end', handleError);
logs.pipe(jsonStream);
function handleError(error?: Error | string) {
logger.logWarn(DeviceConnectionLostError.defaultMsg); logger.logWarn(DeviceConnectionLostError.defaultMsg);
if (gotSignal) { if (gotSignal) {
reject(new SIGINTError('Log streaming aborted on SIGINT signal')); reject(new SIGINTError('Log streaming aborted on SIGINT signal'));
} else { } else {
reject(new DeviceConnectionLostError()); const msg = typeof error === 'string' ? error : error?.message;
reject(new DeviceConnectionLostError(msg));
} }
}); }
logs.pipe(jsonStream);
}); });
} finally { } finally {
process.removeListener('SIGINT', handleSignal); process.removeListener('SIGINT', handleSignal);

View File

@ -72,7 +72,9 @@ export const dockerConnectionCliFlags: flags.Input<DockerConnectionCliFlags> = {
export const dockerCliFlags: flags.Input<DockerCliFlags> = { export const dockerCliFlags: flags.Input<DockerCliFlags> = {
tag: flags.string({ tag: flags.string({
description: 'The alias to the generated image', description: `\
Tag locally built Docker images. This is the 'tag' portion
in 'projectName_serviceName:tag'. The default is 'latest'.`,
char: 't', char: 't',
}), }),
buildArg: flags.string({ buildArg: flags.string({
@ -105,7 +107,7 @@ export interface BuildOpts {
pull?: boolean; pull?: boolean;
registryconfig?: import('resin-multibuild').RegistrySecrets; registryconfig?: import('resin-multibuild').RegistrySecrets;
squash?: boolean; squash?: boolean;
t?: string; t?: string; // only the tag portion of the image name, e.g. 'abc' in 'myimg:abc'
} }
function parseBuildArgs(args: string[]): Dictionary<string> { function parseBuildArgs(args: string[]): Dictionary<string> {

View File

@ -148,23 +148,18 @@ export async function osProgressHandler(step: InitializeEmitter) {
}); });
} }
export function getAppWithArch( export async function getAppWithArch(
applicationName: string, applicationName: string,
): Promise<ApplicationWithDeviceType & { arch: string }> { ): Promise<ApplicationWithDeviceType & { arch: string }> {
return Promise.all([ const app = await getApplication(applicationName);
getApplication(applicationName), const { getExpanded } = await import('./pine');
getBalenaSdk().models.config.getDeviceTypes(),
]).then(function ([app, deviceTypes]) {
const config = _.find<BalenaSdk.DeviceTypeJson.DeviceType>(deviceTypes, {
slug: app.is_for__device_type[0].slug,
});
if (!config) { return {
throw new Error(`balena API request failed for fleet ${applicationName}`); ...app,
} arch: getExpanded(
getExpanded(app.is_for__device_type)!.is_of__cpu_architecture,
return { ...app, arch: config.arch }; )!.slug,
}); };
} }
// TODO: Drop this. The sdk now has this baked in application.get(). // TODO: Drop this. The sdk now has this baked in application.get().
@ -182,6 +177,11 @@ function getApplication(
}, },
is_for__device_type: { is_for__device_type: {
$select: 'slug', $select: 'slug',
$expand: {
is_of__cpu_architecture: {
$select: 'slug',
},
},
}, },
}, },
}; };
@ -463,13 +463,27 @@ export function getProxyConfig(): ProxyConfig | undefined {
} }
} }
export const expandForAppName: BalenaSdk.PineOptions<BalenaSdk.Device> = { export const expandForAppName = {
$expand: { $expand: {
belongs_to__application: { $select: 'app_name' }, belongs_to__application: { $select: 'app_name' },
is_of__device_type: { $select: 'slug' }, is_of__device_type: { $select: 'slug' },
is_running__release: { $select: 'commit' }, is_running__release: { $select: 'commit' },
}, },
}; } as const;
export const expandForAppNameAndCpuArch = {
$expand: {
...expandForAppName.$expand,
is_of__device_type: {
$select: 'slug',
$expand: {
is_of__cpu_architecture: {
$select: 'slug',
},
},
},
},
} as const;
/** /**
* Use the `readline` library on Windows to install SIGINT handlers. * Use the `readline` library on Windows to install SIGINT handlers.

View File

@ -27,6 +27,7 @@ import { ExpectedError } from '../errors';
const { toPosixPath } = MultiBuild.PathUtils; const { toPosixPath } = MultiBuild.PathUtils;
// v13: delete this enum
export enum IgnoreFileType { export enum IgnoreFileType {
DockerIgnore, DockerIgnore,
GitIgnore, GitIgnore,
@ -42,6 +43,8 @@ interface IgnoreEntry {
* This class is used by the CLI v10 / v11 "original" tarDirectory function * This class is used by the CLI v10 / v11 "original" tarDirectory function
* in `compose.js`. It is still around for the benefit of the `--gitignore` * in `compose.js`. It is still around for the benefit of the `--gitignore`
* option, but is expected to be deleted in CLI v13. * option, but is expected to be deleted in CLI v13.
*
* v13: delete this class
*/ */
export class FileIgnorer { export class FileIgnorer {
private dockerIgnoreEntries: IgnoreEntry[]; private dockerIgnoreEntries: IgnoreEntry[];

View File

@ -30,6 +30,14 @@ enum Level {
LIVEPUSH = 'livepush', LIVEPUSH = 'livepush',
} }
interface LoggerAdapter {
debug: (msg: string) => void;
error: (msg: string) => void;
info: (msg: string) => void;
log: (msg: string) => void;
warn: (msg: string) => void;
}
/** /**
* General purpose logger class with support for log streams and colours. * General purpose logger class with support for log streams and colours.
* Call `Logger.getLogger()` to retrieve a global shared instance of this * Call `Logger.getLogger()` to retrieve a global shared instance of this
@ -57,6 +65,8 @@ class Logger {
protected deferredLogMessages: Array<[string, Level]>; protected deferredLogMessages: Array<[string, Level]>;
protected adapter: LoggerAdapter;
protected constructor() { protected constructor() {
const logger = new StreamLogger(); const logger = new StreamLogger();
const chalk = getChalk(); const chalk = getChalk();
@ -91,6 +101,14 @@ class Logger {
this.formatMessage = logger.formatWithPrefix.bind(logger); this.formatMessage = logger.formatWithPrefix.bind(logger);
this.deferredLogMessages = []; this.deferredLogMessages = [];
this.adapter = {
debug: (msg: string) => this.logDebug(msg),
error: (msg: string) => this.logError(msg),
info: (msg: string) => this.logInfo(msg),
log: (msg: string) => this.logLogs(msg),
warn: (msg: string) => this.logWarn(msg),
};
} }
protected static logger: Logger; protected static logger: Logger;
@ -151,6 +169,10 @@ class Logger {
}); });
this.deferredLogMessages = []; this.deferredLogMessages = [];
} }
public getAdapter(): LoggerAdapter {
return this.adapter;
}
} }
export = Logger; export = Logger;

View File

@ -15,6 +15,8 @@
* limitations under the License. * limitations under the License.
*/ */
import { isV13 } from './version';
export const reachingOut = `\ export const reachingOut = `\
For further help or support, visit: For further help or support, visit:
https://www.balena.io/docs/reference/balena-cli/#support-faq-and-troubleshooting https://www.balena.io/docs/reference/balena-cli/#support-faq-and-troubleshooting
@ -30,6 +32,12 @@ export const help = reachingOut;
// is parsed, so its evaluation cannot happen at module loading time. // is parsed, so its evaluation cannot happen at module loading time.
export const getHelp = () => (process.env.DEBUG ? '' : debugHint) + help; export const getHelp = () => (process.env.DEBUG ? '' : debugHint) + help;
export const deprecationPolicyNote = `\
The balena CLI enforces its deprecation policy by exiting with an error a year
after the release of the next major version, unless the --unsupported option is
used. Find out more at: https://git.io/JRHUW#deprecation-policy
`;
/** /**
* Take a multiline string like: * Take a multiline string like:
* Line One * Line One
@ -41,8 +49,9 @@ export const getHelp = () => (process.env.DEBUG ? '' : debugHint) + help;
* --------------- * ---------------
* where the length of the dash rows matches the length of the longest line. * where the length of the dash rows matches the length of the longest line.
*/ */
export function warnify(msg: string) { export function warnify(msg: string, prefix = '[Warn] ') {
const lines = msg.split('\n').map((l) => `[Warn] ${l}`); let lines = msg.split('\n');
lines = prefix ? lines.map((l) => `${prefix}${l}`) : lines;
const maxLength = Math.max(...lines.map((l) => l.length)); const maxLength = Math.max(...lines.map((l) => l.length));
const hr = '-'.repeat(maxLength); const hr = '-'.repeat(maxLength);
return [hr, ...lines, hr].join('\n'); return [hr, ...lines, hr].join('\n');
@ -79,7 +88,7 @@ If the --registry-secrets option is not specified, and a secrets.yml or
secrets.json file exists in the balena directory (usually $HOME/.balena), secrets.json file exists in the balena directory (usually $HOME/.balena),
this file will be used instead.`; this file will be used instead.`;
export const dockerignoreHelp = const dockerignoreHelpV12 =
'DOCKERIGNORE AND GITIGNORE FILES \n' + 'DOCKERIGNORE AND GITIGNORE FILES \n' +
`By default, the balena CLI will use a single ".dockerignore" file (if any) at `By default, the balena CLI will use a single ".dockerignore" file (if any) at
the project root (--source directory) in order to decide which source files to the project root (--source directory) in order to decide which source files to
@ -132,6 +141,60 @@ adding counter patterns to the applicable .dockerignore file(s), for example
- https://docs.docker.com/engine/reference/builder/#dockerignore-file - https://docs.docker.com/engine/reference/builder/#dockerignore-file
- https://www.npmjs.com/package/@balena/dockerignore`; - https://www.npmjs.com/package/@balena/dockerignore`;
const dockerignoreHelpV13 =
'DOCKERIGNORE AND GITIGNORE FILES \n' +
`By default, the balena CLI will use a single ".dockerignore" file (if any) at
the project root (--source directory) in order to decide which source files to
exclude from the "build context" (tar stream) sent to balenaCloud, Docker
daemon or balenaEngine. In a microservices (multicontainer) fleet, the
source directory is the directory that contains the "docker-compose.yml" file.
The --multi-dockerignore (-m) option may be used with microservices
(multicontainer) fleets that define a docker-compose.yml file. When this
option is used, each service subdirectory (defined by the \`build\` or
\`build.context\` service properties in the docker-compose.yml file) is
filtered separately according to a .dockerignore file defined in the service
subdirectory. If no .dockerignore file exists in a service subdirectory, then
only the default .dockerignore patterns (see below) apply for that service
subdirectory.
When the --multi-dockerignore (-m) option is used, the .dockerignore file (if
any) defined at the overall project root will be used to filter files and
subdirectories other than service subdirectories. It will not have any effect
on service subdirectories, whether or not a service subdirectory defines its
own .dockerignore file. Multiple .dockerignore files are not merged or added
together, and cannot override or extend other files. This behavior maximizes
compatibility with the standard docker-compose tool, while still allowing a
root .dockerignore file (at the overall project root) to filter files and
folders that are outside service subdirectories.
balena CLI v11 also took .gitignore files into account. This behavior was
deprecated in CLI v12 and removed in CLI v13. Please use .dockerignore files
instead.
Default .dockerignore patterns \n` +
`A few default/hardcoded dockerignore patterns are "merged" (in memory) with the
patterns found in the applicable .dockerignore files, in the following order:
\`\`\`
**/.git
< user's patterns from the applicable '.dockerignore' file, if any >
!**/.balena
!**/.resin
!**/Dockerfile
!**/Dockerfile.*
!**/docker-compose.yml
\`\`\`
These patterns always apply, whether or not .dockerignore files exist in the
project. If necessary, the effect of the \`**/.git\` pattern may be modified by
adding exception patterns to the applicable .dockerignore file(s), for example
\`!mysubmodule/.git\`. For documentation on pattern format, see:
- https://docs.docker.com/engine/reference/builder/#dockerignore-file
- https://www.npmjs.com/package/@balena/dockerignore`;
export const dockerignoreHelp = isV13()
? dockerignoreHelpV13
: dockerignoreHelpV12;
export const applicationIdInfo = `\ export const applicationIdInfo = `\
Fleets may be specified by fleet name, slug, or numeric ID. Fleet slugs are Fleets may be specified by fleet name, slug, or numeric ID. Fleet slugs are
the recommended option, as they are unique and unambiguous. Slugs can be the recommended option, as they are unique and unambiguous. Slugs can be
@ -145,6 +208,16 @@ environments). Numeric fleet IDs are deprecated because they consist of an
implementation detail of the balena backend. We intend to remove support for implementation detail of the balena backend. We intend to remove support for
numeric IDs at some point in the future.`; numeric IDs at some point in the future.`;
export const applicationNameNote = `\
Fleets may be specified by fleet name or slug. Slugs are recommended because
they are unique and unambiguous. Slugs can be listed with the \`balena fleets\`
command. Note that slugs may change if the fleet is renamed. Fleet names are
not unique and may result in "Fleet is ambiguous" errors at any time (even if
"it used to work in the past"), for example if the name clashes with a newly
created public/open fleet, or with fleets from other balena accounts that you
may be invited to join under any role. For this reason, fleet names are
especially discouraged in scripts (e.g. CI environments).`;
export const jsonInfo = `\ export const jsonInfo = `\
The --json option is recommended when scripting the output of this command, The --json option is recommended when scripting the output of this command,
because field names are less likely to change in JSON format and because it because field names are less likely to change in JSON format and because it
@ -184,3 +257,13 @@ the next major version of the CLI (v13). The --v13 option may be used
to enable the new names already now, and suppress a warning message. to enable the new names already now, and suppress a warning message.
(The --v13 option will be silently ignored in CLI v13.) (The --v13 option will be silently ignored in CLI v13.)
Find out more at: https://git.io/JRuZr`; Find out more at: https://git.io/JRuZr`;
export function getNodeEngineVersionWarn(
version: string,
validVersions: string,
) {
version = version.startsWith('v') ? version.substring(1) : version;
return warnify(`\
Node.js version "${version}" does not satisfy requirement "${validVersions}"
This may cause unexpected behavior.`);
}

View File

@ -16,18 +16,12 @@ limitations under the License.
import type * as BalenaSdk from 'balena-sdk'; import type * as BalenaSdk from 'balena-sdk';
import _ = require('lodash'); import _ = require('lodash');
import { import { instanceOf, NotLoggedInError, ExpectedError } from '../errors';
exitWithExpectedError,
instanceOf,
NotLoggedInError,
ExpectedError,
} from '../errors';
import { getBalenaSdk, getVisuals, stripIndent, getCliForm } from './lazy'; import { getBalenaSdk, getVisuals, stripIndent, getCliForm } from './lazy';
import validation = require('./validation'); import validation = require('./validation');
import { delay } from './helpers'; import { delay } from './helpers';
import { isV13 } from './version'; import { isV13 } from './version';
import type { Application, Device, Organization } from 'balena-sdk'; import type { Application, Device, Organization } from 'balena-sdk';
import { getApplication } from './sdk';
export function authenticate(options: {}): Promise<void> { export function authenticate(options: {}): Promise<void> {
const balena = getBalenaSdk(); const balena = getBalenaSdk();
@ -135,18 +129,16 @@ export function selectDeviceType() {
/** /**
* Display interactive confirmation prompt. * Display interactive confirmation prompt.
* If the user declines, then either an error will be thrown, * Throw ExpectedError if the user declines.
* or `exitWithExpectedError` will be called (if exitIfDeclined true).
* @param yesOption - automatically confirm if true * @param yesOption - automatically confirm if true
* @param message - message to display with prompt * @param message - message to display with prompt
* @param yesMessage - message to display if automatically confirming * @param yesMessage - message to display if automatically confirming
* @param exitIfDeclined - exitWithExpectedError when decline if true
*/ */
export async function confirm( export async function confirm(
yesOption: boolean, yesOption: boolean,
message: string, message: string,
yesMessage?: string, yesMessage?: string,
exitIfDeclined = false, defaultValue = false,
) { ) {
if (yesOption) { if (yesOption) {
if (yesMessage) { if (yesMessage) {
@ -162,16 +154,11 @@ export async function confirm(
const confirmed = await getCliForm().ask<boolean>({ const confirmed = await getCliForm().ask<boolean>({
message, message,
type: 'confirm', type: 'confirm',
default: false, default: defaultValue,
}); });
if (!confirmed) { if (!confirmed) {
const err = new ExpectedError('Aborted'); throw new ExpectedError('Aborted');
// TODO remove this deprecated function (exitWithExpectedError)
if (exitIfDeclined) {
exitWithExpectedError(err);
}
throw err;
} }
} }
@ -281,11 +268,9 @@ export async function awaitDeviceOsUpdate(
} }
if (osUpdateStatus.error) { if (osUpdateStatus.error) {
console.error( throw new ExpectedError(
`Failed to complete Host OS update on device ${deviceName}!`, `Failed to complete Host OS update on device ${deviceName}\n${osUpdateStatus.error}`,
); );
exitWithExpectedError(osUpdateStatus.error);
return;
} }
if (osUpdateProgress !== null) { if (osUpdateProgress !== null) {
@ -379,6 +364,7 @@ export async function getOnlineTargetDeviceUuid(
let app: Application; let app: Application;
try { try {
logger.logDebug(`Fetching fleet ${applicationOrDevice}`); logger.logDebug(`Fetching fleet ${applicationOrDevice}`);
const { getApplication } = await import('./sdk');
app = await getApplication(sdk, applicationOrDevice); app = await getApplication(sdk, applicationOrDevice);
} catch (err) { } catch (err) {
const { BalenaApplicationNotFound } = await import('balena-errors'); const { BalenaApplicationNotFound } = await import('balena-errors');

View File

@ -19,6 +19,7 @@ import type * as BalenaSdk from 'balena-sdk';
import { ExpectedError, printErrorMessage } from '../errors'; import { ExpectedError, printErrorMessage } from '../errors';
import { getVisuals, stripIndent, getCliForm } from './lazy'; import { getVisuals, stripIndent, getCliForm } from './lazy';
import Logger = require('./logger'); import Logger = require('./logger');
import { confirm } from './patterns';
import { exec, execBuffered, getDeviceOsRelease } from './ssh'; import { exec, execBuffered, getDeviceOsRelease } from './ssh';
const MIN_BALENAOS_VERSION = 'v2.14.0'; const MIN_BALENAOS_VERSION = 'v2.14.0';
@ -211,7 +212,7 @@ async function getOrSelectApplication(
.value(); .value();
if (!appName) { if (!appName) {
return createOrSelectAppOrExit(sdk, compatibleDeviceTypes, deviceType); return createOrSelectApp(sdk, compatibleDeviceTypes, deviceType);
} }
const options: BalenaSdk.PineOptions<BalenaSdk.Application> = { const options: BalenaSdk.PineOptions<BalenaSdk.Application> = {
@ -239,17 +240,14 @@ async function getOrSelectApplication(
)) as ApplicationWithDeviceType[]; )) as ApplicationWithDeviceType[];
if (applications.length === 0) { if (applications.length === 0) {
const shouldCreateApp = await getCliForm().ask({ await confirm(
message: false,
`No fleet found with name "${appName}".\n` + `No fleet found with name "${appName}".\n` +
'Would you like to create it now?', 'Would you like to create it now?',
type: 'confirm', undefined,
default: true, true,
}); );
if (shouldCreateApp) { return await createApplication(sdk, deviceType, name);
return createApplication(sdk, deviceType, name);
}
process.exit(1);
} }
// We've found at least one fleet with the given name. // We've found at least one fleet with the given name.
@ -269,10 +267,7 @@ async function getOrSelectApplication(
return selectAppFromList(applications); return selectAppFromList(applications);
} }
// TODO: revisit this function's purpose. It was refactored out of async function createOrSelectApp(
// `getOrSelectApplication` above in order to satisfy some resin-lint v3
// rules, but it looks like there's a fair amount of duplicate logic.
async function createOrSelectAppOrExit(
sdk: BalenaSdk.BalenaSDK, sdk: BalenaSdk.BalenaSDK,
compatibleDeviceTypes: string[], compatibleDeviceTypes: string[],
deviceType: string, deviceType: string,
@ -291,17 +286,14 @@ async function createOrSelectAppOrExit(
})) as ApplicationWithDeviceType[]; })) as ApplicationWithDeviceType[];
if (applications.length === 0) { if (applications.length === 0) {
const shouldCreateApp = await getCliForm().ask({ await confirm(
message: false,
'You have no fleets this device can join.\n' + 'You have no fleets this device can join.\n' +
'Would you like to create one now?', 'Would you like to create one now?',
type: 'confirm', undefined,
default: true, true,
}); );
if (shouldCreateApp) { return await createApplication(sdk, deviceType);
return createApplication(sdk, deviceType);
}
process.exit(1);
} }
return selectAppFromList(applications); return selectAppFromList(applications);

View File

@ -21,7 +21,7 @@ import { ExpectedError } from '../errors';
import { getBalenaSdk, stripIndent } from './lazy'; import { getBalenaSdk, stripIndent } from './lazy';
import Logger = require('./logger'); import Logger = require('./logger');
export const QEMU_VERSION = 'v5.2.0+balena4'; export const QEMU_VERSION = 'v6.0.0+balena1';
export const QEMU_BIN_NAME = 'qemu-execve'; export const QEMU_BIN_NAME = 'qemu-execve';
export function qemuPathInContext(context: string) { export function qemuPathInContext(context: string) {

View File

@ -42,6 +42,7 @@ export interface BuildOpts {
headless: boolean; headless: boolean;
convertEol: boolean; convertEol: boolean;
multiDockerignore: boolean; multiDockerignore: boolean;
isDraft: boolean;
} }
export interface RemoteBuild { export interface RemoteBuild {
@ -49,7 +50,7 @@ export interface RemoteBuild {
source: string; source: string;
auth: string; auth: string;
baseUrl: string; baseUrl: string;
nogitignore: boolean; nogitignore: boolean; // v13: delete this line
opts: BuildOpts; opts: BuildOpts;
sdk: BalenaSDK; sdk: BalenaSDK;
// For internal use // For internal use
@ -92,6 +93,7 @@ async function getBuilderEndpoint(
emulated: opts.emulated, emulated: opts.emulated,
nocache: opts.nocache, nocache: opts.nocache,
headless: opts.headless, headless: opts.headless,
isdraft: opts.isDraft,
}); });
// Note that using https (rather than http) is a requirement when using the // Note that using https (rather than http) is a requirement when using the
// --registry-secrets feature, as the secrets are not otherwise encrypted. // --registry-secrets feature, as the secrets are not otherwise encrypted.
@ -319,7 +321,7 @@ async function getTarStream(build: RemoteBuild): Promise<Stream.Readable> {
preFinalizeCallback: preFinalizeCb, preFinalizeCallback: preFinalizeCb,
convertEol: build.opts.convertEol, convertEol: build.opts.convertEol,
multiDockerignore: build.opts.multiDockerignore, multiDockerignore: build.opts.multiDockerignore,
nogitignore: build.nogitignore, nogitignore: build.nogitignore, // v13: delete this line
}); });
} finally { } finally {
tarSpinner.stop(); tarSpinner.stop();

606
npm-shrinkwrap.json generated
View File

@ -1,6 +1,6 @@
{ {
"name": "balena-cli", "name": "balena-cli",
"version": "12.45.0", "version": "12.50.2",
"lockfileVersion": 1, "lockfileVersion": 1,
"requires": true, "requires": true,
"dependencies": { "dependencies": {
@ -1300,12 +1300,12 @@
"integrity": "sha512-u86QDMtkpHLlvehs3Z+yHklXRhDPL5XGCO3BCSuaD61gKzrNDUIj03cz8T/PBPPUJqn7DfWkf9sKP9VwlvxKuw==" "integrity": "sha512-u86QDMtkpHLlvehs3Z+yHklXRhDPL5XGCO3BCSuaD61gKzrNDUIj03cz8T/PBPPUJqn7DfWkf9sKP9VwlvxKuw=="
}, },
"@balena/node-web-streams": { "@balena/node-web-streams": {
"version": "0.2.3", "version": "0.2.4",
"resolved": "https://registry.npmjs.org/@balena/node-web-streams/-/node-web-streams-0.2.3.tgz", "resolved": "https://registry.npmjs.org/@balena/node-web-streams/-/node-web-streams-0.2.4.tgz",
"integrity": "sha512-WaFtrO5lQUAWmLVcBn7V0tLHOuX/S9JPxmhfcEc9drLZhNUKF/psnNwWGfhPxgRwwik4hK0AqKIbjqkLLBXhSg==", "integrity": "sha512-Q9By3GPzANMZuf1i5i7Agyh6BUe6tTa+VCCZzsFzU32iXMcuDRXYHbNIKESrcjVXxiZScPB4u++WPw4LRyK1Gg==",
"requires": { "requires": {
"is-stream": "^1.1.0", "is-stream": "^1.1.0",
"web-streams-polyfill": "^1.3.2" "web-streams-polyfill": "^3.1.0"
} }
}, },
"@balena/udif": { "@balena/udif": {
@ -2169,68 +2169,68 @@
"integrity": "sha512-STcqSvk+c7ArMrZgYxhM92p6O6F7t0SUbGr+zm8s9fJple5EdJAMwP3dXqgdXeF95xWhBpha5kjEqNAIdI0r4w==" "integrity": "sha512-STcqSvk+c7ArMrZgYxhM92p6O6F7t0SUbGr+zm8s9fJple5EdJAMwP3dXqgdXeF95xWhBpha5kjEqNAIdI0r4w=="
}, },
"@sentry/core": { "@sentry/core": {
"version": "5.25.0", "version": "6.13.2",
"resolved": "https://registry.npmjs.org/@sentry/core/-/core-5.25.0.tgz", "resolved": "https://registry.npmjs.org/@sentry/core/-/core-6.13.2.tgz",
"integrity": "sha512-hY6Zmo7t/RV+oZuvXHP6nyAj/QnZr2jW0e7EbL5YKMV8q0vlnjcE0LgqFXme726OJemoLk67z+sQOJic/Ztehg==", "integrity": "sha512-snXNNFLwlS7yYxKTX4DBXebvJK+6ikBWN6noQ1CHowvM3ReFBlrdrs0Z0SsSFEzXm2S4q7f6HHbm66GSQZ/8FQ==",
"requires": { "requires": {
"@sentry/hub": "5.25.0", "@sentry/hub": "6.13.2",
"@sentry/minimal": "5.25.0", "@sentry/minimal": "6.13.2",
"@sentry/types": "5.25.0", "@sentry/types": "6.13.2",
"@sentry/utils": "5.25.0", "@sentry/utils": "6.13.2",
"tslib": "^1.9.3" "tslib": "^1.9.3"
}, },
"dependencies": { "dependencies": {
"tslib": { "tslib": {
"version": "1.14.0", "version": "1.14.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.0.tgz", "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
"integrity": "sha512-+Zw5lu0D9tvBMjGP8LpvMb0u2WW2QV3y+D8mO6J+cNzCYIN4sVy43Bf9vl92nqFahutN0I8zHa7cc4vihIshnw==" "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="
} }
} }
}, },
"@sentry/hub": { "@sentry/hub": {
"version": "5.25.0", "version": "6.13.2",
"resolved": "https://registry.npmjs.org/@sentry/hub/-/hub-5.25.0.tgz", "resolved": "https://registry.npmjs.org/@sentry/hub/-/hub-6.13.2.tgz",
"integrity": "sha512-kOlOiJV8wMX50lYpzMlOXBoH7MNG0Ho4RTusdZnXZBaASq5/ljngDJkLr6uylNjceZQP21wzipCQajsJMYB7EQ==", "integrity": "sha512-sppSuJdNMiMC/vFm/dQowCBh11uTrmvks00fc190YWgxHshodJwXMdpc+pN61VSOmy2QA4MbQ5aMAgHzPzel3A==",
"requires": { "requires": {
"@sentry/types": "5.25.0", "@sentry/types": "6.13.2",
"@sentry/utils": "5.25.0", "@sentry/utils": "6.13.2",
"tslib": "^1.9.3" "tslib": "^1.9.3"
}, },
"dependencies": { "dependencies": {
"tslib": { "tslib": {
"version": "1.14.0", "version": "1.14.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.0.tgz", "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
"integrity": "sha512-+Zw5lu0D9tvBMjGP8LpvMb0u2WW2QV3y+D8mO6J+cNzCYIN4sVy43Bf9vl92nqFahutN0I8zHa7cc4vihIshnw==" "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="
} }
} }
}, },
"@sentry/minimal": { "@sentry/minimal": {
"version": "5.25.0", "version": "6.13.2",
"resolved": "https://registry.npmjs.org/@sentry/minimal/-/minimal-5.25.0.tgz", "resolved": "https://registry.npmjs.org/@sentry/minimal/-/minimal-6.13.2.tgz",
"integrity": "sha512-9JFKuW7U+1vPO86k3+XRtJyooiVZsVOsFFO4GulBzepi3a0ckNyPgyjUY1saLH+cEHx18hu8fGgajvI8ANUF2g==", "integrity": "sha512-6iJfEvHzzpGBHDfLxSHcGObh73XU1OSQKWjuhDOe7UQDyI4BQmTfcXAC+Fr8sm8C/tIsmpVi/XJhs8cubFdSMw==",
"requires": { "requires": {
"@sentry/hub": "5.25.0", "@sentry/hub": "6.13.2",
"@sentry/types": "5.25.0", "@sentry/types": "6.13.2",
"tslib": "^1.9.3" "tslib": "^1.9.3"
}, },
"dependencies": { "dependencies": {
"tslib": { "tslib": {
"version": "1.14.0", "version": "1.14.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.0.tgz", "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
"integrity": "sha512-+Zw5lu0D9tvBMjGP8LpvMb0u2WW2QV3y+D8mO6J+cNzCYIN4sVy43Bf9vl92nqFahutN0I8zHa7cc4vihIshnw==" "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="
} }
} }
}, },
"@sentry/node": { "@sentry/node": {
"version": "5.25.0", "version": "6.13.2",
"resolved": "https://registry.npmjs.org/@sentry/node/-/node-5.25.0.tgz", "resolved": "https://registry.npmjs.org/@sentry/node/-/node-6.13.2.tgz",
"integrity": "sha512-zxoUVdAFTeK9kdEGY95TMs6g8Zx/P55HxG4gHD80BG/XIEvWiGPcGCLOspO4IdGqYXkGS74KfBOIXmmCawWwLg==", "integrity": "sha512-0Vw22amG143MTiNaSny66YGU3+uW7HxyGI9TLGE7aJY1nNmC0DE+OgqQYGBRCrrPu+VFXRDxrOg9b15A1gKqjA==",
"requires": { "requires": {
"@sentry/core": "5.25.0", "@sentry/core": "6.13.2",
"@sentry/hub": "5.25.0", "@sentry/hub": "6.13.2",
"@sentry/tracing": "5.25.0", "@sentry/tracing": "6.13.2",
"@sentry/types": "5.25.0", "@sentry/types": "6.13.2",
"@sentry/utils": "5.25.0", "@sentry/utils": "6.13.2",
"cookie": "^0.4.1", "cookie": "^0.4.1",
"https-proxy-agent": "^5.0.0", "https-proxy-agent": "^5.0.0",
"lru_map": "^0.3.3", "lru_map": "^0.3.3",
@ -2238,49 +2238,49 @@
}, },
"dependencies": { "dependencies": {
"tslib": { "tslib": {
"version": "1.14.0", "version": "1.14.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.0.tgz", "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
"integrity": "sha512-+Zw5lu0D9tvBMjGP8LpvMb0u2WW2QV3y+D8mO6J+cNzCYIN4sVy43Bf9vl92nqFahutN0I8zHa7cc4vihIshnw==" "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="
} }
} }
}, },
"@sentry/tracing": { "@sentry/tracing": {
"version": "5.25.0", "version": "6.13.2",
"resolved": "https://registry.npmjs.org/@sentry/tracing/-/tracing-5.25.0.tgz", "resolved": "https://registry.npmjs.org/@sentry/tracing/-/tracing-6.13.2.tgz",
"integrity": "sha512-KcyHEGFpqSDubHrdWT/vF2hKkjw/ts6NpJ6tPDjBXUNz98BHdAyMKtLOFTCeJFply7/s5fyiAYu44M+M6IG3Bw==", "integrity": "sha512-bHJz+C/nd6biWTNcYAu91JeRilsvVgaye4POkdzWSmD0XoLWHVMrpCQobGpXe7onkp2noU3YQjhqgtBqPHtnpw==",
"requires": { "requires": {
"@sentry/hub": "5.25.0", "@sentry/hub": "6.13.2",
"@sentry/minimal": "5.25.0", "@sentry/minimal": "6.13.2",
"@sentry/types": "5.25.0", "@sentry/types": "6.13.2",
"@sentry/utils": "5.25.0", "@sentry/utils": "6.13.2",
"tslib": "^1.9.3" "tslib": "^1.9.3"
}, },
"dependencies": { "dependencies": {
"tslib": { "tslib": {
"version": "1.14.0", "version": "1.14.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.0.tgz", "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
"integrity": "sha512-+Zw5lu0D9tvBMjGP8LpvMb0u2WW2QV3y+D8mO6J+cNzCYIN4sVy43Bf9vl92nqFahutN0I8zHa7cc4vihIshnw==" "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="
} }
} }
}, },
"@sentry/types": { "@sentry/types": {
"version": "5.25.0", "version": "6.13.2",
"resolved": "https://registry.npmjs.org/@sentry/types/-/types-5.25.0.tgz", "resolved": "https://registry.npmjs.org/@sentry/types/-/types-6.13.2.tgz",
"integrity": "sha512-8M4PREbcar+15wrtEqcwfcU33SS+2wBSIOd/NrJPXJPTYxi49VypCN1mZBDyWkaK+I+AuQwI3XlRPCfsId3D1A==" "integrity": "sha512-6WjGj/VjjN8LZDtqJH5ikeB1o39rO1gYS6anBxiS3d0sXNBb3Ux0pNNDFoBxQpOhmdDHXYS57MEptX9EV82gmg=="
}, },
"@sentry/utils": { "@sentry/utils": {
"version": "5.25.0", "version": "6.13.2",
"resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-5.25.0.tgz", "resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-6.13.2.tgz",
"integrity": "sha512-Hz5spdIkMSRH5NR1YFOp5qbsY5Ud2lKhEQWlqxcVThMG5YNUc10aYv5ijL19v0YkrC2rqPjCRm7GrVtzOc7bXQ==", "integrity": "sha512-foF4PbxqPMWNbuqdXkdoOmKm3quu3PP7Q7j/0pXkri4DtCuvF/lKY92mbY0V9rHS/phCoj+3/Se5JvM2ymh2/w==",
"requires": { "requires": {
"@sentry/types": "5.25.0", "@sentry/types": "6.13.2",
"tslib": "^1.9.3" "tslib": "^1.9.3"
}, },
"dependencies": { "dependencies": {
"tslib": { "tslib": {
"version": "1.14.0", "version": "1.14.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.0.tgz", "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
"integrity": "sha512-+Zw5lu0D9tvBMjGP8LpvMb0u2WW2QV3y+D8mO6J+cNzCYIN4sVy43Bf9vl92nqFahutN0I8zHa7cc4vihIshnw==" "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="
} }
} }
}, },
@ -2385,6 +2385,17 @@
"@types/node": "*" "@types/node": "*"
} }
}, },
"@types/cacheable-request": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.2.tgz",
"integrity": "sha512-B3xVo+dlKM6nnKTcmm5ZtY/OL8bOAOd2Olee9M1zft65ox50OzjEHW91sDiU9j6cvW8Ejg1/Qkf4xd2kugApUA==",
"requires": {
"@types/http-cache-semantics": "*",
"@types/keyv": "*",
"@types/node": "*",
"@types/responselike": "*"
}
},
"@types/caseless": { "@types/caseless": {
"version": "0.12.2", "version": "0.12.2",
"resolved": "https://registry.npmjs.org/@types/caseless/-/caseless-0.12.2.tgz", "resolved": "https://registry.npmjs.org/@types/caseless/-/caseless-0.12.2.tgz",
@ -2534,6 +2545,11 @@
"integrity": "sha512-TikApqV8CkUsI1GGUgVydkJFrq9sYCBWv4fc/r3zvl6Oqe2YU1ASeWBrG5bw1D2XvS07YS3s05hCor/lEtIoYw==", "integrity": "sha512-TikApqV8CkUsI1GGUgVydkJFrq9sYCBWv4fc/r3zvl6Oqe2YU1ASeWBrG5bw1D2XvS07YS3s05hCor/lEtIoYw==",
"dev": true "dev": true
}, },
"@types/http-cache-semantics": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.1.tgz",
"integrity": "sha512-SZs7ekbP8CN0txVG2xVRH6EgKmEm31BOxA07vkFaETzZz1xh+cbt8BcI0slpymvwhx5dlFnQG2rTlPVQn+iRPQ=="
},
"@types/http-proxy": { "@types/http-proxy": {
"version": "1.17.7", "version": "1.17.7",
"resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.7.tgz", "resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.7.tgz",
@ -2600,6 +2616,14 @@
"resolved": "https://registry.npmjs.org/@types/jwt-decode/-/jwt-decode-2.2.1.tgz", "resolved": "https://registry.npmjs.org/@types/jwt-decode/-/jwt-decode-2.2.1.tgz",
"integrity": "sha512-aWw2YTtAdT7CskFyxEX2K21/zSDStuf/ikI3yBqmwpwJF0pS+/IX5DWv+1UFffZIbruP6cnT9/LAJV1gFwAT1A==" "integrity": "sha512-aWw2YTtAdT7CskFyxEX2K21/zSDStuf/ikI3yBqmwpwJF0pS+/IX5DWv+1UFffZIbruP6cnT9/LAJV1gFwAT1A=="
}, },
"@types/keyv": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.2.tgz",
"integrity": "sha512-/FvAK2p4jQOaJ6CGDHJTqZcUtbZe820qIeTg7o0Shg7drB4JHeL+V/dhSaly7NXx6u8eSee+r7coT+yuJEvDLg==",
"requires": {
"@types/node": "*"
}
},
"@types/klaw": { "@types/klaw": {
"version": "3.0.2", "version": "3.0.2",
"resolved": "https://registry.npmjs.org/@types/klaw/-/klaw-3.0.2.tgz", "resolved": "https://registry.npmjs.org/@types/klaw/-/klaw-3.0.2.tgz",
@ -2615,14 +2639,14 @@
"integrity": "sha512-7eQ2xYLLI/LsicL2nejW9Wyko3lcpN6O/z0ZLHrEQsg280zIdCv1t/0m6UtBjUHokCGBQ3gYTbHzDkZ1xOBwwg==" "integrity": "sha512-7eQ2xYLLI/LsicL2nejW9Wyko3lcpN6O/z0ZLHrEQsg280zIdCv1t/0m6UtBjUHokCGBQ3gYTbHzDkZ1xOBwwg=="
}, },
"@types/lru-cache": { "@types/lru-cache": {
"version": "5.1.0", "version": "5.1.1",
"resolved": "https://registry.npmjs.org/@types/lru-cache/-/lru-cache-5.1.0.tgz", "resolved": "https://registry.npmjs.org/@types/lru-cache/-/lru-cache-5.1.1.tgz",
"integrity": "sha512-RaE0B+14ToE4l6UqdarKPnXwVDuigfFv+5j9Dze/Nqr23yyuqdNvzcZi3xB+3Agvi5R4EOgAksfv3lXX4vBt9w==" "integrity": "sha512-ssE3Vlrys7sdIzs5LOxCzTVMsU7i9oa/IaW92wF32JFb3CVczqOkru2xspuKczHEbG3nvmPY7IFqVmGGHdNbYw=="
}, },
"@types/memoizee": { "@types/memoizee": {
"version": "0.4.5", "version": "0.4.6",
"resolved": "https://registry.npmjs.org/@types/memoizee/-/memoizee-0.4.5.tgz", "resolved": "https://registry.npmjs.org/@types/memoizee/-/memoizee-0.4.6.tgz",
"integrity": "sha512-+ZzZZ3+0a7/ajBPeAAD4+LxrBsCat0EFZQtO3o0rwpIeLmDmSaM8KF/oYPuFxeUFAMiHIHFcGucFnY/8S4Hszg==" "integrity": "sha512-qJezGqoi3pW9Pset2w1Gfv8jATvmHHHnpO9Dq8x8pJGyYIpiUZJqRU0NM7xenmN0AcXEe7vqshI8H98KeFLYcg=="
}, },
"@types/mime": { "@types/mime": {
"version": "1.3.2", "version": "1.3.2",
@ -2791,6 +2815,14 @@
} }
} }
}, },
"@types/responselike": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.0.tgz",
"integrity": "sha512-85Y2BjiufFzaMIlvJDvTTB8Fxl2xfLo4HgmHzVBz08w4wDePCTjYw66PdrolO0kzli3yam/YCgRufyo1DdQVTA==",
"requires": {
"@types/node": "*"
}
},
"@types/rewire": { "@types/rewire": {
"version": "2.5.28", "version": "2.5.28",
"resolved": "https://registry.npmjs.org/@types/rewire/-/rewire-2.5.28.tgz", "resolved": "https://registry.npmjs.org/@types/rewire/-/rewire-2.5.28.tgz",
@ -2905,9 +2937,9 @@
} }
}, },
"@types/uuid": { "@types/uuid": {
"version": "8.3.0", "version": "8.3.1",
"resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-8.3.0.tgz", "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-8.3.1.tgz",
"integrity": "sha512-eQ9qFW/fhfGJF8WKHGEHZEyVWfZxrT+6CLIJGBcZPfxUh/+BnEj+UCGYMlr9qZuX/2AltsvwrGqp0LhEW8D0zQ==" "integrity": "sha512-Y2mHTRAbqfFkpjldbkHGY8JIzRN6XqYRliG8/24FcHm2D2PwW24fl5xMRTVGdrb7iMrwCaIEbLWerGIkXuFWVg=="
}, },
"@types/which": { "@types/which": {
"version": "2.0.1", "version": "2.0.1",
@ -3061,9 +3093,9 @@
"dev": true "dev": true
}, },
"agent-base": { "agent-base": {
"version": "6.0.1", "version": "6.0.2",
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.1.tgz", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
"integrity": "sha512-01q25QQDwLSsyfhrKbn8yuur+JNw0H+0Y4JiGIKd3z9aYk/w/2kxD/Upc+t2ZBBSUNff50VjPsSW2YxM8QYKVg==", "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==",
"requires": { "requires": {
"debug": "4" "debug": "4"
} }
@ -3732,9 +3764,9 @@
} }
}, },
"balena-hup-action-utils": { "balena-hup-action-utils": {
"version": "4.0.2", "version": "4.0.3",
"resolved": "https://registry.npmjs.org/balena-hup-action-utils/-/balena-hup-action-utils-4.0.2.tgz", "resolved": "https://registry.npmjs.org/balena-hup-action-utils/-/balena-hup-action-utils-4.0.3.tgz",
"integrity": "sha512-N2HVaqXodwR18HKnbOwJDzDZOLpQoPzH6MD4Ibs09Sb9OGBizgjAHiK4Qs20qX1wvztqm8sy8zWJ8nXEOFAplg==", "integrity": "sha512-PdHMpSjaQriB4y4zmeAmm0Mxudencc/BVjI3jHVH3/SCZ6OqIIGlyFJU2tS0vsghED8PiEyQSjUdDCGzv7zUiQ==",
"requires": { "requires": {
"balena-semver": "^2.0.0", "balena-semver": "^2.0.0",
"tslib": "^2.0.0" "tslib": "^2.0.0"
@ -3779,22 +3811,12 @@
"balena-errors": "^4.2.1", "balena-errors": "^4.2.1",
"pinejs-client-core": "^6.9.0", "pinejs-client-core": "^6.9.0",
"tslib": "^2.0.1" "tslib": "^2.0.1"
},
"dependencies": {
"pinejs-client-core": {
"version": "6.9.5",
"resolved": "https://registry.npmjs.org/pinejs-client-core/-/pinejs-client-core-6.9.5.tgz",
"integrity": "sha512-/QbmrR6IjlGZiM3JjZq9WtMdoR5UWW4zI+mCElm6Ez6kZiBYnWbHEDaVpyfEAj3mmSf6bj9ezQTxU+eLnzC1OQ==",
"requires": {
"@balena/es-version": "^1.0.0"
}
}
} }
}, },
"balena-preload": { "balena-preload": {
"version": "10.4.20", "version": "10.5.0",
"resolved": "https://registry.npmjs.org/balena-preload/-/balena-preload-10.4.20.tgz", "resolved": "https://registry.npmjs.org/balena-preload/-/balena-preload-10.5.0.tgz",
"integrity": "sha512-uKbccD1oh7BQ9XUMXYQvGklfws1v9+oaWkYxE0eheh+0GcHP+CVWr/TIUx+P3eplh7cAfeZAxw0wx0jp8H1Ttg==", "integrity": "sha512-tgnTyOSOLB3HxIqlR1NFrTsy1eiiew5Vzmplb82/eZc/vJTrOqal2tFNn6aFay6UQ8+OASUJwANC99zBu1e8mQ==",
"requires": { "requires": {
"archiver": "^3.1.1", "archiver": "^3.1.1",
"balena-sdk": "^15.44.0", "balena-sdk": "^15.44.0",
@ -3827,32 +3849,6 @@
"zip-stream": "^2.1.2" "zip-stream": "^2.1.2"
} }
}, },
"balena-sdk": {
"version": "15.45.0",
"resolved": "https://registry.npmjs.org/balena-sdk/-/balena-sdk-15.45.0.tgz",
"integrity": "sha512-lfz6x8yL7nV+zHTC2+w30Dtv7I5rz8IxoO6Ifjb4/ewkhz6LyX1/UhH/a4k83oaQMXIuoYnmwWE6iVx/zl9xXQ==",
"requires": {
"@balena/es-version": "^1.0.0",
"@types/lodash": "^4.14.168",
"@types/memoizee": "^0.4.5",
"@types/node": "^10.17.55",
"abortcontroller-polyfill": "^1.7.1",
"balena-auth": "^4.1.0",
"balena-errors": "^4.7.1",
"balena-hup-action-utils": "~4.0.2",
"balena-pine": "^12.4.0",
"balena-register-device": "^7.1.0",
"balena-request": "^11.4.0",
"balena-semver": "^2.3.0",
"balena-settings-client": "^4.0.6",
"lodash": "^4.17.21",
"memoizee": "^0.4.15",
"moment": "^2.29.1",
"ndjson": "^2.0.0",
"semver": "^7.3.4",
"tslib": "^2.1.0"
}
},
"compress-commons": { "compress-commons": {
"version": "2.1.1", "version": "2.1.1",
"resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-2.1.1.tgz", "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-2.1.1.tgz",
@ -3938,9 +3934,9 @@
} }
}, },
"tslib": { "tslib": {
"version": "2.3.0", "version": "2.3.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz",
"integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==" "integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw=="
}, },
"zip-stream": { "zip-stream": {
"version": "2.1.3", "version": "2.1.3",
@ -3966,16 +3962,16 @@
}, },
"dependencies": { "dependencies": {
"tslib": { "tslib": {
"version": "2.2.0", "version": "2.3.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.2.0.tgz", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz",
"integrity": "sha512-gS9GVHRU+RGn5KQM2rllAlR3dU6m7AcpJKdtH8gFvQiC4Otgk98XnmMU+nZenHt/+VhnBPWwgrJsyrdcw6i23w==" "integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw=="
} }
} }
}, },
"balena-release": { "balena-release": {
"version": "3.0.0", "version": "3.2.0",
"resolved": "https://registry.npmjs.org/balena-release/-/balena-release-3.0.0.tgz", "resolved": "https://registry.npmjs.org/balena-release/-/balena-release-3.2.0.tgz",
"integrity": "sha512-LYgPGBUnqJY+ajhTWE2BizyaRNxitxPSIZp4xGWDHbpVHmwKaV4p3d6nw1Hf9kEHP4dLELJvnNXq2EKV2IpTFA==", "integrity": "sha512-jwmAjIZCJ5I46/yQNN+dA73RWlre0+jBVmo2QeJl1pK83obTLyifJeWNVf5irzP8KFE7WQzo9ICK1cCpLtygFA==",
"requires": { "requires": {
"@types/bluebird": "^3.5.18", "@types/bluebird": "^3.5.18",
"@types/node": "^8.0.55", "@types/node": "^8.0.55",
@ -3987,16 +3983,16 @@
}, },
"dependencies": { "dependencies": {
"@types/node": { "@types/node": {
"version": "8.10.62", "version": "8.10.66",
"resolved": "https://registry.npmjs.org/@types/node/-/node-8.10.62.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-8.10.66.tgz",
"integrity": "sha512-76fupxOYVxk36kb7O/6KtrAPZ9jnSK3+qisAX4tQMEuGNdlvl7ycwatlHqjoE6jHfVtXFM3pCrCixZOidc5cuw==" "integrity": "sha512-tktOkFUA4kXx2hhhrB8bIFb5TbwzS4uOhKEmwiD+NoiL0qtP2OQ9mFldbgD4dV1djrlBYP6eBuQZiWjuHUpqFw=="
} }
} }
}, },
"balena-request": { "balena-request": {
"version": "11.4.0", "version": "11.4.2",
"resolved": "https://registry.npmjs.org/balena-request/-/balena-request-11.4.0.tgz", "resolved": "https://registry.npmjs.org/balena-request/-/balena-request-11.4.2.tgz",
"integrity": "sha512-wfPaWX/+NgT2xNplQqA8oCNLJXG6eLMbf9IOX8T4ZX+nqBoA9bydoIRLunGExMNfUWpxApvBh5ls8fJOd9VTjQ==", "integrity": "sha512-J4SrFBUR4AB2Y3afsX2QAMZ7H/zjysXjOyEhEqvjNTsBfe5ReCmf17vzRQ8q2URCm00nUhQgfQtlJUq6miB1/g==",
"requires": { "requires": {
"@balena/node-web-streams": "^0.2.3", "@balena/node-web-streams": "^0.2.3",
"balena-errors": "^4.7.1", "balena-errors": "^4.7.1",
@ -4008,9 +4004,9 @@
} }
}, },
"balena-sdk": { "balena-sdk": {
"version": "15.36.0", "version": "15.51.1",
"resolved": "https://registry.npmjs.org/balena-sdk/-/balena-sdk-15.36.0.tgz", "resolved": "https://registry.npmjs.org/balena-sdk/-/balena-sdk-15.51.1.tgz",
"integrity": "sha512-h55dSJpZ8XCJAwvbfinCuGPQXfSIBg/ftkpd6ObV+WQ/iN7DIpBKG0QVPdWoK91Ws0Ie/GzmW4IFvgtS/Yf3hQ==", "integrity": "sha512-EMCQruytqyvpfxvjq9Zd/wWnnOAIl/Wd1majqv6hqa+z104UUTjnRCQaUji4mo8YtrFLn7aUUZFFHIKiv/3sTg==",
"requires": { "requires": {
"@balena/es-version": "^1.0.0", "@balena/es-version": "^1.0.0",
"@types/lodash": "^4.14.168", "@types/lodash": "^4.14.168",
@ -4022,7 +4018,7 @@
"balena-hup-action-utils": "~4.0.2", "balena-hup-action-utils": "~4.0.2",
"balena-pine": "^12.4.0", "balena-pine": "^12.4.0",
"balena-register-device": "^7.1.0", "balena-register-device": "^7.1.0",
"balena-request": "^11.4.0", "balena-request": "^11.4.2",
"balena-semver": "^2.3.0", "balena-semver": "^2.3.0",
"balena-settings-client": "^4.0.6", "balena-settings-client": "^4.0.6",
"lodash": "^4.17.21", "lodash": "^4.17.21",
@ -4047,9 +4043,9 @@
} }
}, },
"tslib": { "tslib": {
"version": "2.2.0", "version": "2.3.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.2.0.tgz", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz",
"integrity": "sha512-gS9GVHRU+RGn5KQM2rllAlR3dU6m7AcpJKdtH8gFvQiC4Otgk98XnmMU+nZenHt/+VhnBPWwgrJsyrdcw6i23w==" "integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw=="
} }
} }
}, },
@ -4065,9 +4061,9 @@
} }
}, },
"balena-settings-client": { "balena-settings-client": {
"version": "4.0.6", "version": "4.0.7",
"resolved": "https://registry.npmjs.org/balena-settings-client/-/balena-settings-client-4.0.6.tgz", "resolved": "https://registry.npmjs.org/balena-settings-client/-/balena-settings-client-4.0.7.tgz",
"integrity": "sha512-bB14Zvg1N6t7XXPJqZs48SajgTuk2WTMm2AnxcOfoIQ2d/Lh0RsEGxD9toF2v+WhF2Ip4u7ko5tKlCr2kFddXA==", "integrity": "sha512-1ncEgufbAbzcfcffsTpi20asNdsOEZxACiQhv8naQp1mgw6INe/0FvSNX6St+XlXtuk1FqCnYNINGIjMoStOrA==",
"requires": { "requires": {
"@resin.io/types-hidepath": "1.0.1", "@resin.io/types-hidepath": "1.0.1",
"@resin.io/types-home-or-tmp": "3.0.0", "@resin.io/types-home-or-tmp": "3.0.0",
@ -4597,6 +4593,11 @@
"unset-value": "^1.0.0" "unset-value": "^1.0.0"
} }
}, },
"cacheable-lookup": {
"version": "5.0.4",
"resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz",
"integrity": "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA=="
},
"cacheable-request": { "cacheable-request": {
"version": "6.1.0", "version": "6.1.0",
"resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-6.1.0.tgz", "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-6.1.0.tgz",
@ -6496,6 +6497,14 @@
} }
} }
}, },
"dockerfile-ast": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/dockerfile-ast/-/dockerfile-ast-0.2.1.tgz",
"integrity": "sha512-ut04CVM1G6zIITTcYPDIXhPZk9mCa21m4dfW8FcDDGxwgTQhYyHDu6U7M8klZ7QsjqVcJhryKi+TGOX6bjgKdQ==",
"requires": {
"vscode-languageserver-types": "^3.16.0"
}
},
"dockerfile-template": { "dockerfile-template": {
"version": "0.2.0", "version": "0.2.0",
"resolved": "https://registry.npmjs.org/dockerfile-template/-/dockerfile-template-0.2.0.tgz", "resolved": "https://registry.npmjs.org/dockerfile-template/-/dockerfile-template-0.2.0.tgz",
@ -6899,19 +6908,6 @@
"es6-symbol": "^3.1.1" "es6-symbol": "^3.1.1"
} }
}, },
"es6-promise": {
"version": "4.2.8",
"resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz",
"integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w=="
},
"es6-promisify": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/es6-promisify/-/es6-promisify-5.0.0.tgz",
"integrity": "sha1-UQnWLz5W6pZ8S2NQWu8IKRyKUgM=",
"requires": {
"es6-promise": "^4.0.3"
}
},
"es6-symbol": { "es6-symbol": {
"version": "3.1.3", "version": "3.1.3",
"resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.3.tgz", "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.3.tgz",
@ -7778,7 +7774,6 @@
"version": "4.0.3", "version": "4.0.3",
"resolved": "https://registry.npmjs.org/ffi-napi/-/ffi-napi-4.0.3.tgz", "resolved": "https://registry.npmjs.org/ffi-napi/-/ffi-napi-4.0.3.tgz",
"integrity": "sha512-PMdLCIvDY9mS32RxZ0XGb95sonPRal8aqRhLbeEtWKZTe2A87qRFG9HjOhvG8EX2UmQw5XNRMIOT+1MYlWmdeg==", "integrity": "sha512-PMdLCIvDY9mS32RxZ0XGb95sonPRal8aqRhLbeEtWKZTe2A87qRFG9HjOhvG8EX2UmQw5XNRMIOT+1MYlWmdeg==",
"optional": true,
"requires": { "requires": {
"debug": "^4.1.1", "debug": "^4.1.1",
"get-uv-event-loop-napi-h": "^1.0.5", "get-uv-event-loop-napi-h": "^1.0.5",
@ -7789,10 +7784,9 @@
}, },
"dependencies": { "dependencies": {
"node-addon-api": { "node-addon-api": {
"version": "3.1.0", "version": "3.2.1",
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-3.1.0.tgz", "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-3.2.1.tgz",
"integrity": "sha512-flmrDNB06LIl5lywUz7YlNGZH/5p0M7W28k8hzd9Lshtdh1wshD2Y+U4h9LD6KObOy1f+fEVdgprPrEymjM5uw==", "integrity": "sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A=="
"optional": true
} }
} }
}, },
@ -8203,9 +8197,9 @@
"integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ=" "integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ="
}, },
"fp-ts": { "fp-ts": {
"version": "2.10.5", "version": "2.11.3",
"resolved": "https://registry.npmjs.org/fp-ts/-/fp-ts-2.10.5.tgz", "resolved": "https://registry.npmjs.org/fp-ts/-/fp-ts-2.11.3.tgz",
"integrity": "sha512-X2KfTIV0cxIk3d7/2Pvp/pxL/xr2MV1WooyEzKtTWYSc1+52VF4YzjBTXqeOlSiZsPCxIBpDGfT9Dyo7WEY0DQ==" "integrity": "sha512-qHI5iaVSFNFmdl6yDensWfFMk32iafAINCnqx8m486DV1+Jht/bTnA9CyahL+Xm7h2y3erinviVBIAWvv5bPYw=="
}, },
"fragment-cache": { "fragment-cache": {
"version": "0.2.1", "version": "0.2.1",
@ -8433,14 +8427,12 @@
"get-symbol-from-current-process-h": { "get-symbol-from-current-process-h": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/get-symbol-from-current-process-h/-/get-symbol-from-current-process-h-1.0.2.tgz", "resolved": "https://registry.npmjs.org/get-symbol-from-current-process-h/-/get-symbol-from-current-process-h-1.0.2.tgz",
"integrity": "sha512-syloC6fsCt62ELLrr1VKBM1ggOpMdetX9hTrdW77UQdcApPHLmf7CI7OKcN1c9kYuNxKcDe4iJ4FY9sX3aw2xw==", "integrity": "sha512-syloC6fsCt62ELLrr1VKBM1ggOpMdetX9hTrdW77UQdcApPHLmf7CI7OKcN1c9kYuNxKcDe4iJ4FY9sX3aw2xw=="
"optional": true
}, },
"get-uv-event-loop-napi-h": { "get-uv-event-loop-napi-h": {
"version": "1.0.6", "version": "1.0.6",
"resolved": "https://registry.npmjs.org/get-uv-event-loop-napi-h/-/get-uv-event-loop-napi-h-1.0.6.tgz", "resolved": "https://registry.npmjs.org/get-uv-event-loop-napi-h/-/get-uv-event-loop-napi-h-1.0.6.tgz",
"integrity": "sha512-t5c9VNR84nRoF+eLiz6wFrEp1SE2Acg0wS+Ysa2zF0eROes+LzOfuTaVHxGy8AbS8rq7FHEJzjnCZo1BupwdJg==", "integrity": "sha512-t5c9VNR84nRoF+eLiz6wFrEp1SE2Acg0wS+Ysa2zF0eROes+LzOfuTaVHxGy8AbS8rq7FHEJzjnCZo1BupwdJg==",
"optional": true,
"requires": { "requires": {
"get-symbol-from-current-process-h": "^1.0.1" "get-symbol-from-current-process-h": "^1.0.1"
} }
@ -8465,6 +8457,31 @@
"requires": { "requires": {
"got": "^6.2.0", "got": "^6.2.0",
"is-plain-obj": "^1.1.0" "is-plain-obj": "^1.1.0"
},
"dependencies": {
"get-stream": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz",
"integrity": "sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ="
},
"got": {
"version": "6.7.1",
"resolved": "https://registry.npmjs.org/got/-/got-6.7.1.tgz",
"integrity": "sha1-JAzQV4WpoY5WHcG0S0HHY+8ejbA=",
"requires": {
"create-error-class": "^3.0.0",
"duplexer3": "^0.1.4",
"get-stream": "^3.0.0",
"is-redirect": "^1.0.0",
"is-retry-allowed": "^1.0.0",
"is-stream": "^1.0.0",
"lowercase-keys": "^1.0.0",
"safe-buffer": "^5.0.1",
"timed-out": "^4.0.0",
"unzip-response": "^2.0.1",
"url-parse-lax": "^1.0.0"
}
}
} }
}, },
"ghauth": { "ghauth": {
@ -8941,27 +8958,111 @@
} }
}, },
"got": { "got": {
"version": "6.7.1", "version": "11.8.2",
"resolved": "https://registry.npmjs.org/got/-/got-6.7.1.tgz", "resolved": "https://registry.npmjs.org/got/-/got-11.8.2.tgz",
"integrity": "sha1-JAzQV4WpoY5WHcG0S0HHY+8ejbA=", "integrity": "sha512-D0QywKgIe30ODs+fm8wMZiAcZjypcCodPNuMz5H9Mny7RJ+IjJ10BdmGW7OM7fHXP+O7r6ZwapQ/YQmMSvB0UQ==",
"requires": { "requires": {
"create-error-class": "^3.0.0", "@sindresorhus/is": "^4.0.0",
"duplexer3": "^0.1.4", "@szmarczak/http-timer": "^4.0.5",
"get-stream": "^3.0.0", "@types/cacheable-request": "^6.0.1",
"is-redirect": "^1.0.0", "@types/responselike": "^1.0.0",
"is-retry-allowed": "^1.0.0", "cacheable-lookup": "^5.0.3",
"is-stream": "^1.0.0", "cacheable-request": "^7.0.1",
"lowercase-keys": "^1.0.0", "decompress-response": "^6.0.0",
"safe-buffer": "^5.0.1", "http2-wrapper": "^1.0.0-beta.5.2",
"timed-out": "^4.0.0", "lowercase-keys": "^2.0.0",
"unzip-response": "^2.0.1", "p-cancelable": "^2.0.0",
"url-parse-lax": "^1.0.0" "responselike": "^2.0.0"
}, },
"dependencies": { "dependencies": {
"@sindresorhus/is": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.0.1.tgz",
"integrity": "sha512-Qm9hBEBu18wt1PO2flE7LPb30BHMQt1eQgbV76YntdNk73XZGpn3izvGTYxbGgzXKgbCjiia0uxTd3aTNQrY/g=="
},
"@szmarczak/http-timer": {
"version": "4.0.6",
"resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz",
"integrity": "sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==",
"requires": {
"defer-to-connect": "^2.0.0"
}
},
"cacheable-request": {
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-7.0.2.tgz",
"integrity": "sha512-pouW8/FmiPQbuGpkXQ9BAPv/Mo5xDGANgSNXzTzJ8DrKGuXOssM4wIQRjfanNRh3Yu5cfYPvcorqbhg2KIJtew==",
"requires": {
"clone-response": "^1.0.2",
"get-stream": "^5.1.0",
"http-cache-semantics": "^4.0.0",
"keyv": "^4.0.0",
"lowercase-keys": "^2.0.0",
"normalize-url": "^6.0.1",
"responselike": "^2.0.0"
}
},
"decompress-response": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz",
"integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==",
"requires": {
"mimic-response": "^3.1.0"
}
},
"defer-to-connect": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz",
"integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg=="
},
"get-stream": { "get-stream": {
"version": "3.0.0", "version": "5.2.0",
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz",
"integrity": "sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ=" "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==",
"requires": {
"pump": "^3.0.0"
}
},
"json-buffer": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
"integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="
},
"keyv": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.0.3.tgz",
"integrity": "sha512-zdGa2TOpSZPq5mU6iowDARnMBZgtCqJ11dJROFi6tg6kTn4nuUdU09lFyLFSaHrWqpIJ+EBq4E8/Dc0Vx5vLdA==",
"requires": {
"json-buffer": "3.0.1"
}
},
"lowercase-keys": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz",
"integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA=="
},
"mimic-response": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz",
"integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ=="
},
"normalize-url": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz",
"integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A=="
},
"p-cancelable": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.1.1.tgz",
"integrity": "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg=="
},
"responselike": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/responselike/-/responselike-2.0.0.tgz",
"integrity": "sha512-xH48u3FTB9VsZw7R+vvgaKeLKzT6jOogbQhEe/jewwnZgzPcnyWui2Av6JpoYZF/91uueC+lqhWqeURw5/qhCw==",
"requires": {
"lowercase-keys": "^2.0.0"
}
} }
} }
}, },
@ -9424,6 +9525,15 @@
"sshpk": "^1.7.0" "sshpk": "^1.7.0"
} }
}, },
"http2-wrapper": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.3.tgz",
"integrity": "sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==",
"requires": {
"quick-lru": "^5.1.1",
"resolve-alpn": "^1.0.0"
}
},
"https-proxy-agent": { "https-proxy-agent": {
"version": "5.0.0", "version": "5.0.0",
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz",
@ -11870,41 +11980,6 @@
} }
} }
}, },
"mixpanel": {
"version": "0.10.3",
"resolved": "https://registry.npmjs.org/mixpanel/-/mixpanel-0.10.3.tgz",
"integrity": "sha512-wIYr5o+1XSzJ80o3QED35K/yfPAKi5FigZXTSfcs4vltfeKbilIjNgwxdno7LrqzhjoSjmIyDWkI7D3lr7TwDw==",
"requires": {
"https-proxy-agent": "3.0.0"
},
"dependencies": {
"agent-base": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-4.3.0.tgz",
"integrity": "sha512-salcGninV0nPrwpGNn4VTXBb1SOuXQBiqbrNXoeizJsHrsL6ERFM2Ne3JUSBWRE6aeNJI2ROP/WEEIDUiDe3cg==",
"requires": {
"es6-promisify": "^5.0.0"
}
},
"debug": {
"version": "3.2.6",
"resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz",
"integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==",
"requires": {
"ms": "^2.1.1"
}
},
"https-proxy-agent": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-3.0.0.tgz",
"integrity": "sha512-y4jAxNEihqvBI5F3SaO2rtsjIOnnNA8sEbuiP+UhJZJHeM2NRm6c09ax2tgqme+SgUUvjao2fJXF4h3D6Cb2HQ==",
"requires": {
"agent-base": "^4.3.0",
"debug": "^3.1.0"
}
}
}
},
"mkdirp": { "mkdirp": {
"version": "0.5.5", "version": "0.5.5",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz",
@ -12471,10 +12546,9 @@
} }
}, },
"net-keepalive": { "net-keepalive": {
"version": "2.0.3", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/net-keepalive/-/net-keepalive-2.0.3.tgz", "resolved": "https://registry.npmjs.org/net-keepalive/-/net-keepalive-3.0.0.tgz",
"integrity": "sha512-VNWUXuLR0tUkZT2VVanJ5VqlBTJL8O5tGVsgq/FrvhHO7vjVC8gfU0ogRL+hxnfYuzSkMaiTHZfdIqwu7Q4zXA==", "integrity": "sha512-wfDa7VPeSltY5aIQcujS7AiWnO2JHJCpO3is4nwQ7kFYs4YMpzDNMwiuILPkWwgMbPMSHzO7O1tuL8rC0SP3ag==",
"optional": true,
"requires": { "requires": {
"ffi-napi": "^4.0.1", "ffi-napi": "^4.0.1",
"ref-napi": "^3.0.0" "ref-napi": "^3.0.0"
@ -12565,8 +12639,7 @@
"node-gyp-build": { "node-gyp-build": {
"version": "4.2.3", "version": "4.2.3",
"resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.2.3.tgz", "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.2.3.tgz",
"integrity": "sha512-MN6ZpzmfNCRM+3t57PTJHgHyw/h4OWnZ6mR8P5j/uZtqQr46RRuDE/P+g3n0YR/AiYXeWixZZzaip77gdICfRg==", "integrity": "sha512-MN6ZpzmfNCRM+3t57PTJHgHyw/h4OWnZ6mR8P5j/uZtqQr46RRuDE/P+g3n0YR/AiYXeWixZZzaip77gdICfRg=="
"optional": true
}, },
"node-pre-gyp": { "node-pre-gyp": {
"version": "0.14.0", "version": "0.14.0",
@ -13806,24 +13879,24 @@
"integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=" "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY="
}, },
"pinejs-client-core": { "pinejs-client-core": {
"version": "6.7.3", "version": "6.9.6",
"resolved": "https://registry.npmjs.org/pinejs-client-core/-/pinejs-client-core-6.7.3.tgz", "resolved": "https://registry.npmjs.org/pinejs-client-core/-/pinejs-client-core-6.9.6.tgz",
"integrity": "sha512-VXX/EpbDC/LiEPix/S9gENsaruoa2N0GHzmEbf9jYZ7qhwGNJjGlnQXQ1AnE/cjHYgHOo2RnMYKJ+iBakFgnWQ==", "integrity": "sha512-XUfHeYxT65PIxaV2SWZ7o/2nBUpTg9NaAcrfIRHgMkWQVlcUnh5EguHXoo2nKA4ocKds1fxIheuT29JqEg9SWQ==",
"requires": { "requires": {
"@balena/es-version": "^1.0.0" "@balena/es-version": "^1.0.0"
} }
}, },
"pinejs-client-request": { "pinejs-client-request": {
"version": "7.2.0", "version": "7.3.3",
"resolved": "https://registry.npmjs.org/pinejs-client-request/-/pinejs-client-request-7.2.0.tgz", "resolved": "https://registry.npmjs.org/pinejs-client-request/-/pinejs-client-request-7.3.3.tgz",
"integrity": "sha512-rzUbSc3AkxHKlEz3TeJ5txABoxVsrYGSLjcv9uS3A7oVuObF8vD/CUFredJ/5m3hnvSRRIpRl0nPyugRAqTS+g==", "integrity": "sha512-HmJfI/yvRB5mrwPedSIMhgcdWco1g4BfGa3bIwEoncOAi808y9FhfPrbcBe1ZjWGtGZTZRE020LpkMaIyigzBg==",
"requires": { "requires": {
"@types/lodash": "^4.14.159", "@types/lodash": "^4.14.168",
"@types/lru-cache": "^5.1.0", "@types/lru-cache": "^5.1.0",
"@types/request": "^2.48.5", "@types/request": "^2.48.5",
"lodash": "^4.17.19", "lodash": "^4.17.21",
"lru-cache": "^6.0.0", "lru-cache": "^6.0.0",
"pinejs-client-core": "^6.6.1", "pinejs-client-core": "^6.9.5",
"request": "^2.88.2", "request": "^2.88.2",
"typed-error": "^3.2.1" "typed-error": "^3.2.1"
} }
@ -14712,6 +14785,11 @@
"resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz",
"integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=" "integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA="
}, },
"quick-lru": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz",
"integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA=="
},
"randombytes": { "randombytes": {
"version": "2.1.0", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
@ -15063,22 +15141,20 @@
} }
}, },
"ref-napi": { "ref-napi": {
"version": "3.0.2", "version": "3.0.3",
"resolved": "https://registry.npmjs.org/ref-napi/-/ref-napi-3.0.2.tgz", "resolved": "https://registry.npmjs.org/ref-napi/-/ref-napi-3.0.3.tgz",
"integrity": "sha512-5YE0XrvWteoTr5DR2sEqxefL06aml7c6qS7hGv3u27do4HlGQphwvB+zD1NYep9utMKScvwOZsSs9EPYdGBVsg==", "integrity": "sha512-LiMq/XDGcgodTYOMppikEtJelWsKQERbLQsYm0IOOnzhwE9xYZC7x8txNnFC9wJNOkPferQI4vD4ZkC0mDyrOA==",
"optional": true,
"requires": { "requires": {
"debug": "^4.1.1", "debug": "^4.1.1",
"get-symbol-from-current-process-h": "^1.0.2", "get-symbol-from-current-process-h": "^1.0.2",
"node-addon-api": "^2.0.0", "node-addon-api": "^3.0.0",
"node-gyp-build": "^4.2.1" "node-gyp-build": "^4.2.1"
}, },
"dependencies": { "dependencies": {
"node-addon-api": { "node-addon-api": {
"version": "2.0.2", "version": "3.2.1",
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-2.0.2.tgz", "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-3.2.1.tgz",
"integrity": "sha512-Ntyt4AIXyaLIuMHF6IOoTakB3K+RWxwtsHNRxllEoA6vPwP9o4866g6YWDLUdnucilZhmkxiHwHr11gAENw+QA==", "integrity": "sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A=="
"optional": true
} }
} }
}, },
@ -15086,7 +15162,6 @@
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/ref-struct-di/-/ref-struct-di-1.1.1.tgz", "resolved": "https://registry.npmjs.org/ref-struct-di/-/ref-struct-di-1.1.1.tgz",
"integrity": "sha512-2Xyn/0Qgz89VT+++WP0sTosdm9oeowLP23wRJYhG4BFdMUrLj3jhwHZNEytYNYgtPKLNTP3KJX4HEgBvM1/Y2g==", "integrity": "sha512-2Xyn/0Qgz89VT+++WP0sTosdm9oeowLP23wRJYhG4BFdMUrLj3jhwHZNEytYNYgtPKLNTP3KJX4HEgBvM1/Y2g==",
"optional": true,
"requires": { "requires": {
"debug": "^3.1.0" "debug": "^3.1.0"
}, },
@ -15095,7 +15170,6 @@
"version": "3.2.7", "version": "3.2.7",
"resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
"integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
"optional": true,
"requires": { "requires": {
"ms": "^2.1.1" "ms": "^2.1.1"
} }
@ -15366,9 +15440,9 @@
} }
}, },
"resin-compose-parse": { "resin-compose-parse": {
"version": "2.1.3", "version": "2.3.0",
"resolved": "https://registry.npmjs.org/resin-compose-parse/-/resin-compose-parse-2.1.3.tgz", "resolved": "https://registry.npmjs.org/resin-compose-parse/-/resin-compose-parse-2.3.0.tgz",
"integrity": "sha512-X5WQo+OHoPe+FV8JliGzSIL4glLX0PPFvtnopppYef1UqKcJm+GHaiEZBOj3C7vIEDqQrsNrKXY/BpadlOFiWA==", "integrity": "sha512-9vE+ascGEXuyTYjLmhwmVOYNkws99Cq7/RtUrgT8VCYJRuhqZMpGMOJPrjEIF6PvsBY7B6+u32x1HPCR4gWHBQ==",
"requires": { "requires": {
"@types/lodash": "^4.14.86", "@types/lodash": "^4.14.86",
"@types/node": "^8.0.55", "@types/node": "^8.0.55",
@ -15442,9 +15516,12 @@
} }
}, },
"@types/klaw": { "@types/klaw": {
"version": "1.3.5", "version": "1.3.6",
"resolved": "https://registry.npmjs.org/@types/klaw/-/klaw-1.3.5.tgz", "resolved": "https://registry.npmjs.org/@types/klaw/-/klaw-1.3.6.tgz",
"integrity": "sha512-KZfv4ea6bEbdQhfwpxtDuTPO2mHAAXMQqPOZyS4MgNyCymKoLHp0FVzzYq3H2zCeIotN4h1453TahLCCm8rf2w==" "integrity": "sha512-4pr2RxwhfsLxFYa4Ip8JxrdXIvPX7fAqyBh9ofZPedMwf8M5CIcSQskqvX6/5Y/zpCBHtuC3218t8H+XJsg5FA==",
"requires": {
"@types/node": "*"
}
}, },
"bl": { "bl": {
"version": "1.2.3", "version": "1.2.3",
@ -15641,13 +15718,14 @@
} }
}, },
"resin-multibuild": { "resin-multibuild": {
"version": "4.11.0", "version": "4.12.2",
"resolved": "https://registry.npmjs.org/resin-multibuild/-/resin-multibuild-4.11.0.tgz", "resolved": "https://registry.npmjs.org/resin-multibuild/-/resin-multibuild-4.12.2.tgz",
"integrity": "sha512-rIYV9GDNuI8pU9N+wGdVRIOGAnw1BFdbyt3BkvERFxbf+b/e7jpBjHkbK8VPQdRMlKPyu137ZxQlR3z7EivJBg==", "integrity": "sha512-FkRqGEM588wA6v03pQbodPqWQdAs6aMh+GWvYQBz5IxqSVecn4FLHaRE0pF6VFKtjf/XBuPw7dtqiFzH+NIz5g==",
"requires": { "requires": {
"ajv": "^6.12.3", "ajv": "^6.12.3",
"bluebird": "^3.7.2", "bluebird": "^3.7.2",
"docker-progress": "^5.0.0", "docker-progress": "^5.0.0",
"dockerfile-ast": "^0.2.1",
"dockerfile-template": "^0.2.0", "dockerfile-template": "^0.2.0",
"dockerode": "^2.5.8", "dockerode": "^2.5.8",
"fp-ts": "^2.8.1", "fp-ts": "^2.8.1",
@ -15887,6 +15965,11 @@
"path-parse": "^1.0.6" "path-parse": "^1.0.6"
} }
}, },
"resolve-alpn": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.0.tgz",
"integrity": "sha512-e4FNQs+9cINYMO5NMFc6kOUCdohjqFPSgMuwuZAOUWqrfWsen+Yjy5qZFkV5K7VO7tFSLKcUL97olkED7sCBHA=="
},
"resolve-dir": { "resolve-dir": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/resolve-dir/-/resolve-dir-1.0.1.tgz", "resolved": "https://registry.npmjs.org/resolve-dir/-/resolve-dir-1.0.1.tgz",
@ -16383,13 +16466,13 @@
"dev": true "dev": true
}, },
"sinon": { "sinon": {
"version": "11.1.1", "version": "11.1.2",
"resolved": "https://registry.npmjs.org/sinon/-/sinon-11.1.1.tgz", "resolved": "https://registry.npmjs.org/sinon/-/sinon-11.1.2.tgz",
"integrity": "sha512-ZSSmlkSyhUWbkF01Z9tEbxZLF/5tRC9eojCdFh33gtQaP7ITQVaMWQHGuFM7Cuf/KEfihuh1tTl3/ABju3AQMg==", "integrity": "sha512-59237HChms4kg7/sXhiRcUzdSkKuydDeTiamT/jesUVHshBgL8XAmhgFo0GfK6RruMDM/iRSij1EybmMog9cJw==",
"dev": true, "dev": true,
"requires": { "requires": {
"@sinonjs/commons": "^1.8.3", "@sinonjs/commons": "^1.8.3",
"@sinonjs/fake-timers": "^7.1.0", "@sinonjs/fake-timers": "^7.1.2",
"@sinonjs/samsam": "^6.0.2", "@sinonjs/samsam": "^6.0.2",
"diff": "^5.0.0", "diff": "^5.0.0",
"nise": "^5.1.0", "nise": "^5.1.0",
@ -18408,6 +18491,11 @@
} }
} }
}, },
"vscode-languageserver-types": {
"version": "3.16.0",
"resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.16.0.tgz",
"integrity": "sha512-k8luDIWJWyenLc5ToFQQMaSrqCHiLwyKPHKPQZ5zz21vM+vIVUSvsRpcbiECH4WR88K2XZqc4ScRcZ7nk/jbeA=="
},
"wcwidth": { "wcwidth": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz",
@ -18417,9 +18505,9 @@
} }
}, },
"web-streams-polyfill": { "web-streams-polyfill": {
"version": "1.3.2", "version": "3.1.1",
"resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-1.3.2.tgz", "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.1.1.tgz",
"integrity": "sha1-NxkkXpCSgtk5Z4JfRLzVUOnAOZU=" "integrity": "sha512-Czi3fG883e96T4DLEPRvufrF2ydhOOW1+1a6c3gNjH2aIh50DNFBdfwh2AKoOf1rXvpvavAoA11Qdq9+BKjE0Q=="
}, },
"which": { "which": {
"version": "2.0.2", "version": "2.0.2",

View File

@ -1,6 +1,6 @@
{ {
"name": "balena-cli", "name": "balena-cli",
"version": "12.45.0", "version": "12.50.2",
"description": "The official balena Command Line Interface", "description": "The official balena Command Line Interface",
"main": "./build/app.js", "main": "./build/app.js",
"homepage": "https://github.com/balena-io/balena-cli", "homepage": "https://github.com/balena-io/balena-cli",
@ -49,6 +49,7 @@
"postinstall": "node patches/apply-patches.js", "postinstall": "node patches/apply-patches.js",
"prebuild": "rimraf build/ build-bin/", "prebuild": "rimraf build/ build-bin/",
"build": "npm run build:src && npm run catch-uncommitted", "build": "npm run build:src && npm run catch-uncommitted",
"build:t": "npm run lint && npm run build:fast && npm run build:test",
"build:src": "npm run lint && npm run build:fast && npm run build:test && npm run build:doc && npm run build:completion", "build:src": "npm run lint && npm run build:fast && npm run build:test && npm run build:doc && npm run build:completion",
"build:fast": "gulp pages && tsc && npx oclif-dev manifest", "build:fast": "gulp pages && tsc && npx oclif-dev manifest",
"build:test": "tsc -P ./tsconfig.dev.json --noEmit && tsc -P ./tsconfig.js.json --noEmit", "build:test": "tsc -P ./tsconfig.dev.json --noEmit && tsc -P ./tsconfig.js.json --noEmit",
@ -65,6 +66,7 @@
"test:standalone": "npm run build:standalone && npm run test:standalone:fast", "test:standalone": "npm run build:standalone && npm run test:standalone:fast",
"test:standalone:fast": "cross-env BALENA_CLI_TEST_TYPE=standalone mocha --config .mocharc-standalone.js", "test:standalone:fast": "cross-env BALENA_CLI_TEST_TYPE=standalone mocha --config .mocharc-standalone.js",
"test:fast": "npm run build:fast && npm run test:source", "test:fast": "npm run build:fast && npm run test:source",
"test:debug": "cross-env BALENA_CLI_TEST_TYPE=source mocha --inspect-brk=0.0.0.0",
"test:only": "npm run build:fast && cross-env BALENA_CLI_TEST_TYPE=source mocha \"tests/**/${npm_config_test}.spec.ts\"", "test:only": "npm run build:fast && cross-env BALENA_CLI_TEST_TYPE=source mocha \"tests/**/${npm_config_test}.spec.ts\"",
"catch-uncommitted": "ts-node --transpile-only automation/run.ts catch-uncommitted", "catch-uncommitted": "ts-node --transpile-only automation/run.ts catch-uncommitted",
"ci": "npm run test && npm run catch-uncommitted", "ci": "npm run test && npm run catch-uncommitted",
@ -185,7 +187,7 @@
"publish-release": "^1.6.1", "publish-release": "^1.6.1",
"rewire": "^5.0.0", "rewire": "^5.0.0",
"simple-git": "^2.40.0", "simple-git": "^2.40.0",
"sinon": "^11.1.1", "sinon": "^11.1.2",
"ts-node": "^10.0.0", "ts-node": "^10.0.0",
"typescript": "^4.3.5" "typescript": "^4.3.5"
}, },
@ -194,7 +196,7 @@
"@balena/es-version": "^1.0.0", "@balena/es-version": "^1.0.0",
"@oclif/command": "^1.8.0", "@oclif/command": "^1.8.0",
"@resin.io/valid-email": "^0.1.0", "@resin.io/valid-email": "^0.1.0",
"@sentry/node": "^5.25.0", "@sentry/node": "^6.13.2",
"@types/fast-levenshtein": "0.0.1", "@types/fast-levenshtein": "0.0.1",
"@types/update-notifier": "^4.1.1", "@types/update-notifier": "^4.1.1",
"@zeit/dockerignore": "0.0.3", "@zeit/dockerignore": "0.0.3",
@ -204,11 +206,11 @@
"balena-errors": "^4.7.1", "balena-errors": "^4.7.1",
"balena-image-fs": "^7.0.6", "balena-image-fs": "^7.0.6",
"balena-image-manager": "^7.0.3", "balena-image-manager": "^7.0.3",
"balena-preload": "^10.4.20", "balena-preload": "^10.5.0",
"balena-release": "^3.0.0", "balena-release": "^3.2.0",
"balena-sdk": "^15.36.0", "balena-sdk": "^15.51.1",
"balena-semver": "^2.3.0", "balena-semver": "^2.3.0",
"balena-settings-client": "^4.0.6", "balena-settings-client": "^4.0.7",
"balena-settings-storage": "^7.0.0", "balena-settings-storage": "^7.0.0",
"balena-sync": "^11.0.2", "balena-sync": "^11.0.2",
"bluebird": "^3.7.2", "bluebird": "^3.7.2",
@ -236,6 +238,7 @@
"glob": "^7.1.7", "glob": "^7.1.7",
"global-agent": "^2.1.12", "global-agent": "^2.1.12",
"global-tunnel-ng": "^2.1.1", "global-tunnel-ng": "^2.1.1",
"got": "^11.8.2",
"humanize": "0.0.9", "humanize": "0.0.9",
"ignore": "^5.1.8", "ignore": "^5.1.8",
"inquirer": "^7.3.3", "inquirer": "^7.3.3",
@ -246,10 +249,10 @@
"livepush": "^3.5.0", "livepush": "^3.5.0",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"minimatch": "^3.0.4", "minimatch": "^3.0.4",
"mixpanel": "^0.10.3",
"moment": "^2.27.0", "moment": "^2.27.0",
"moment-duration-format": "^2.3.2", "moment-duration-format": "^2.3.2",
"ndjson": "^2.0.0", "ndjson": "^2.0.0",
"net-keepalive": "^3.0.0",
"node-cleanup": "^2.1.2", "node-cleanup": "^2.1.2",
"node-unzip-2": "^0.2.8", "node-unzip-2": "^0.2.8",
"oclif": "^1.18.1", "oclif": "^1.18.1",
@ -262,9 +265,9 @@
"request": "^2.88.2", "request": "^2.88.2",
"resin-cli-form": "^2.0.2", "resin-cli-form": "^2.0.2",
"resin-cli-visuals": "^1.8.0", "resin-cli-visuals": "^1.8.0",
"resin-compose-parse": "^2.1.3", "resin-compose-parse": "^2.3.0",
"resin-doodles": "^0.1.1", "resin-doodles": "^0.1.1",
"resin-multibuild": "^4.11.0", "resin-multibuild": "^4.12.2",
"resin-stream-logger": "^0.1.2", "resin-stream-logger": "^0.1.2",
"rimraf": "^3.0.2", "rimraf": "^3.0.2",
"semver": "^7.3.2", "semver": "^7.3.2",
@ -283,10 +286,9 @@
"window-size": "^1.1.0" "window-size": "^1.1.0"
}, },
"optionalDependencies": { "optionalDependencies": {
"net-keepalive": "^2.0.3",
"windosu": "^0.3.0" "windosu": "^0.3.0"
}, },
"versionist": { "versionist": {
"publishedAt": "2021-08-09T11:15:24.420Z" "publishedAt": "2021-10-05T09:04:41.226Z"
} }
} }

View File

@ -16,7 +16,7 @@
*/ */
import { expect } from 'chai'; import { expect } from 'chai';
import { BalenaAPIMock } from '../../balena-api-mock'; import { BalenaAPIMock } from '../../nock/balena-api-mock';
import { cleanOutput, runCommand } from '../../helpers'; import { cleanOutput, runCommand } from '../../helpers';
const HELP_MESSAGE = ''; const HELP_MESSAGE = '';

View File

@ -21,10 +21,11 @@ import mock = require('mock-require');
import { promises as fs } from 'fs'; import { promises as fs } from 'fs';
import * as path from 'path'; import * as path from 'path';
import { stripIndent } from '../../lib/utils/lazy'; import { stripIndent } from '../../build/utils/lazy';
import { BalenaAPIMock } from '../balena-api-mock'; import { isV13 } from '../../build/utils/version';
import { BalenaAPIMock } from '../nock/balena-api-mock';
import { expectStreamNoCRLF, testDockerBuildStream } from '../docker-build'; import { expectStreamNoCRLF, testDockerBuildStream } from '../docker-build';
import { DockerMock, dockerResponsePath } from '../docker-mock'; import { DockerMock, dockerResponsePath } from '../nock/docker-mock';
import { cleanOutput, runCommand } from '../helpers'; import { cleanOutput, runCommand } from '../helpers';
import { import {
ExpectedTarStreamFiles, ExpectedTarStreamFiles,
@ -52,6 +53,16 @@ const commonQueryParams = {
labels: '', labels: '',
}; };
const commonQueryParamsIntel = {
...commonQueryParams,
platform: 'linux/amd64',
};
const commonQueryParamsArmV6 = {
...commonQueryParams,
platform: 'linux/arm/v6',
};
const commonComposeQueryParams = { const commonComposeQueryParams = {
t: '${tag}', t: '${tag}',
buildargs: { buildargs: {
@ -61,8 +72,10 @@ const commonComposeQueryParams = {
labels: '', labels: '',
}; };
const hr = const commonComposeQueryParamsIntel = {
'----------------------------------------------------------------------'; ...commonComposeQueryParams,
platform: 'linux/amd64',
};
// "itSS" means "it() Skip Standalone" // "itSS" means "it() Skip Standalone"
const itSS = process.env.BALENA_CLI_TEST_TYPE === 'standalone' ? it.skip : it; const itSS = process.env.BALENA_CLI_TEST_TYPE === 'standalone' ? it.skip : it;
@ -78,7 +91,7 @@ describe('balena build', function () {
api.expectGetWhoAmI({ optional: true, persist: true }); api.expectGetWhoAmI({ optional: true, persist: true });
api.expectGetMixpanel({ optional: true }); api.expectGetMixpanel({ optional: true });
docker.expectGetPing(); docker.expectGetPing();
docker.expectGetVersion(); docker.expectGetVersion({ persist: true });
}); });
this.afterEach(() => { this.afterEach(() => {
@ -125,11 +138,16 @@ describe('balena build', function () {
} }
} }
docker.expectGetInfo({}); docker.expectGetInfo({});
docker.expectGetManifestBusybox();
await testDockerBuildStream({ await testDockerBuildStream({
commandLine: `build ${projectPath} --deviceType nuc --arch amd64 -g`, commandLine: `build ${projectPath} --deviceType nuc --arch amd64 ${
isV13() ? '' : '-g'
}`,
dockerMock: docker, dockerMock: docker,
expectedFilesByService: { main: expectedFiles }, expectedFilesByService: { main: expectedFiles },
expectedQueryParamsByService: { main: Object.entries(commonQueryParams) }, expectedQueryParamsByService: {
main: Object.entries(commonQueryParamsIntel),
},
expectedResponseLines, expectedResponseLines,
projectPath, projectPath,
responseBody, responseBody,
@ -152,7 +170,7 @@ describe('balena build', function () {
'Dockerfile-alt': { fileSize: 30, type: 'file' }, 'Dockerfile-alt': { fileSize: 30, type: 'file' },
}; };
const expectedQueryParams = { const expectedQueryParams = {
...commonQueryParams, ...commonQueryParamsIntel,
buildargs: '{"BARG1":"b1","barg2":"B2"}', buildargs: '{"BARG1":"b1","barg2":"B2"}',
cachefrom: '["my/img1","my/img2"]', cachefrom: '["my/img1","my/img2"]',
}; };
@ -181,6 +199,7 @@ describe('balena build', function () {
} }
} }
docker.expectGetInfo({}); docker.expectGetInfo({});
docker.expectGetManifestBusybox();
await testDockerBuildStream({ await testDockerBuildStream({
commandLine: `build ${projectPath} --deviceType nuc --arch amd64 -B BARG1=b1 -B barg2=B2 --cache-from my/img1,my/img2`, commandLine: `build ${projectPath} --deviceType nuc --arch amd64 -B BARG1=b1 -B barg2=B2 --cache-from my/img1,my/img2`,
dockerMock: docker, dockerMock: docker,
@ -271,12 +290,15 @@ describe('balena build', function () {
}); });
mock.reRequire('../../build/utils/qemu'); mock.reRequire('../../build/utils/qemu');
docker.expectGetInfo({ OperatingSystem: 'balenaOS 2.44.0+rev1' }); docker.expectGetInfo({ OperatingSystem: 'balenaOS 2.44.0+rev1' });
docker.expectGetManifestBusybox();
await testDockerBuildStream({ await testDockerBuildStream({
commandLine: `build ${projectPath} --emulated --deviceType ${deviceType} --arch ${arch} --nogitignore`, commandLine: `build ${projectPath} --emulated --deviceType ${deviceType} --arch ${arch} ${
isV13() ? '' : '--nogitignore'
}`,
dockerMock: docker, dockerMock: docker,
expectedFilesByService: { main: expectedFiles }, expectedFilesByService: { main: expectedFiles },
expectedQueryParamsByService: { expectedQueryParamsByService: {
main: Object.entries(commonQueryParams), main: Object.entries(commonQueryParamsArmV6),
}, },
expectedResponseLines, expectedResponseLines,
projectPath, projectPath,
@ -325,11 +347,15 @@ describe('balena build', function () {
); );
} }
docker.expectGetInfo({}); docker.expectGetInfo({});
docker.expectGetManifestBusybox();
await testDockerBuildStream({ await testDockerBuildStream({
commandLine: `build ${projectPath} --deviceType nuc --arch amd64 --noconvert-eol -m`, commandLine: `build ${projectPath} --deviceType nuc --arch amd64 --noconvert-eol -m`,
dockerMock: docker, dockerMock: docker,
expectedFilesByService: { main: expectedFiles }, expectedFilesByService: { main: expectedFiles },
expectedQueryParamsByService: { main: Object.entries(commonQueryParams) }, expectedQueryParamsByService: {
main: Object.entries(commonQueryParamsIntel),
},
expectedResponseLines, expectedResponseLines,
projectPath, projectPath,
responseBody, responseBody,
@ -358,7 +384,7 @@ describe('balena build', function () {
}, },
service2: { service2: {
'.dockerignore': { fileSize: 12, type: 'file' }, '.dockerignore': { fileSize: 12, type: 'file' },
'Dockerfile-alt': { fileSize: 40, type: 'file' }, 'Dockerfile-alt': { fileSize: 13, type: 'file' },
'file2-crlf.sh': { 'file2-crlf.sh': {
fileSize: isWindows ? 12 : 14, fileSize: isWindows ? 12 : 14,
testStream: isWindows ? expectStreamNoCRLF : undefined, testStream: isWindows ? expectStreamNoCRLF : undefined,
@ -384,7 +410,7 @@ describe('balena build', function () {
}), }),
), ),
service2: Object.entries( service2: Object.entries(
_.merge({}, commonComposeQueryParams, { _.merge({}, commonComposeQueryParamsIntel, {
buildargs: { buildargs: {
COMPOSE_ARG: 'A', COMPOSE_ARG: 'A',
barg: 'b', barg: 'b',
@ -415,8 +441,12 @@ describe('balena build', function () {
); );
} }
docker.expectGetInfo({}); docker.expectGetInfo({});
docker.expectGetManifestNucAlpine();
docker.expectGetManifestBusybox();
await testDockerBuildStream({ await testDockerBuildStream({
commandLine: `build ${projectPath} --deviceType nuc --arch amd64 --convert-eol -G -B COMPOSE_ARG=A -B barg=b --cache-from my/img1,my/img2`, commandLine: `build ${projectPath} --deviceType nuc --arch amd64 --convert-eol ${
isV13() ? '' : '-G'
} -B COMPOSE_ARG=A -B barg=b --cache-from my/img1,my/img2`,
dockerMock: docker, dockerMock: docker,
expectedFilesByService, expectedFilesByService,
expectedQueryParamsByService, expectedQueryParamsByService,
@ -449,7 +479,7 @@ describe('balena build', function () {
}, },
service2: { service2: {
'.dockerignore': { fileSize: 12, type: 'file' }, '.dockerignore': { fileSize: 12, type: 'file' },
'Dockerfile-alt': { fileSize: 40, type: 'file' }, 'Dockerfile-alt': { fileSize: 13, type: 'file' },
'file2-crlf.sh': { 'file2-crlf.sh': {
fileSize: isWindows ? 12 : 14, fileSize: isWindows ? 12 : 14,
testStream: isWindows ? expectStreamNoCRLF : undefined, testStream: isWindows ? expectStreamNoCRLF : undefined,
@ -469,7 +499,7 @@ describe('balena build', function () {
}), }),
), ),
service2: Object.entries( service2: Object.entries(
_.merge({}, commonComposeQueryParams, { _.merge({}, commonComposeQueryParamsIntel, {
buildargs: { buildargs: {
COMPOSE_ARG: 'an argument defined in the docker-compose.yml file', COMPOSE_ARG: 'an argument defined in the docker-compose.yml file',
}, },
@ -484,11 +514,11 @@ describe('balena build', function () {
'[Build] service2 Step 1/4 : FROM busybox', '[Build] service2 Step 1/4 : FROM busybox',
], ],
...[ ...[
`[Info] ${hr}`, `[Info] ---------------------------------------------------------------------------`,
'[Info] The --multi-dockerignore option is being used, and a .dockerignore file was', '[Info] The --multi-dockerignore option is being used, and a .dockerignore file was',
'[Info] found at the project source (root) directory. Note that this file will not', '[Info] found at the project source (root) directory. Note that this file will not',
'[Info] be used to filter service subdirectories. See "balena help build".', '[Info] be used to filter service subdirectories. See "balena help build".',
`[Info] ${hr}`, `[Info] ---------------------------------------------------------------------------`,
], ],
]; ];
if (isWindows) { if (isWindows) {
@ -501,6 +531,9 @@ describe('balena build', function () {
); );
} }
docker.expectGetInfo({}); docker.expectGetInfo({});
docker.expectGetManifestBusybox();
docker.expectGetManifestNucAlpine();
await testDockerBuildStream({ await testDockerBuildStream({
commandLine: `build ${projectPath} --deviceType nuc --arch amd64 --convert-eol -m`, commandLine: `build ${projectPath} --deviceType nuc --arch amd64 --convert-eol -m`,
dockerMock: docker, dockerMock: docker,
@ -513,6 +546,99 @@ describe('balena build', function () {
services: ['service1', 'service2'], services: ['service1', 'service2'],
}); });
}); });
it('should create the expected tar stream (--projectName and --tag)', async () => {
const projectPath = path.join(projectsPath, 'docker-compose', 'basic');
const service1Dockerfile = (
await fs.readFile(
path.join(projectPath, 'service1', 'Dockerfile.template'),
'utf8',
)
).replace('%%BALENA_MACHINE_NAME%%', 'nuc');
const expectedFilesByService: ExpectedTarStreamFilesByService = {
service1: {
Dockerfile: {
contents: service1Dockerfile,
fileSize: service1Dockerfile.length,
type: 'file',
},
'Dockerfile.template': { fileSize: 144, type: 'file' },
'file1.sh': { fileSize: 12, type: 'file' },
'test-ignore.txt': { fileSize: 12, type: 'file' },
},
service2: {
'.dockerignore': { fileSize: 12, type: 'file' },
'Dockerfile-alt': { fileSize: 13, type: 'file' },
'file2-crlf.sh': {
fileSize: isWindows ? 12 : 14,
testStream: isWindows ? expectStreamNoCRLF : undefined,
type: 'file',
},
},
};
const responseFilename = 'build-POST.json';
const responseBody = await fs.readFile(
path.join(dockerResponsePath, responseFilename),
'utf8',
);
const expectedQueryParamsByService = {
service1: Object.entries(
_.merge({}, commonComposeQueryParams, {
buildargs: { SERVICE1_VAR: 'This is a service specific variable' },
}),
),
service2: Object.entries(
_.merge({}, commonComposeQueryParamsIntel, {
buildargs: {
COMPOSE_ARG: 'an argument defined in the docker-compose.yml file',
},
dockerfile: 'Dockerfile-alt',
}),
),
};
const expectedResponseLines: string[] = [
...commonResponseLines[responseFilename],
...[
'[Build] service1 Step 1/4 : FROM busybox',
'[Build] service2 Step 1/4 : FROM busybox',
],
...[
`[Info] ---------------------------------------------------------------------------`,
'[Info] The --multi-dockerignore option is being used, and a .dockerignore file was',
'[Info] found at the project source (root) directory. Note that this file will not',
'[Info] be used to filter service subdirectories. See "balena help build".',
`[Info] ---------------------------------------------------------------------------`,
],
];
if (isWindows) {
expectedResponseLines.push(
`[Info] Converting line endings CRLF -> LF for file: ${path.join(
projectPath,
'service2',
'file2-crlf.sh',
)}`,
);
}
const projectName = 'spectest';
const tag = 'myTag';
docker.expectGetInfo({});
docker.expectGetManifestBusybox();
docker.expectGetManifestNucAlpine();
await testDockerBuildStream({
commandLine: `build ${projectPath} --deviceType nuc --arch amd64 --convert-eol -m --tag ${tag} --projectName ${projectName}`,
dockerMock: docker,
expectedFilesByService,
expectedQueryParamsByService,
expectedResponseLines,
projectName,
projectPath,
responseBody,
responseCode: 200,
services: ['service1', 'service2'],
tag,
});
});
}); });
describe('balena build: project validation', function () { describe('balena build: project validation', function () {

View File

@ -15,15 +15,18 @@
* limitations under the License. * limitations under the License.
*/ */
import type { Request as ReleaseRequest } from 'balena-release';
import { expect } from 'chai'; import { expect } from 'chai';
import { promises as fs } from 'fs'; import { promises as fs } from 'fs';
import * as _ from 'lodash'; import * as _ from 'lodash';
import * as nock from 'nock';
import * as path from 'path'; import * as path from 'path';
import * as sinon from 'sinon'; import * as sinon from 'sinon';
import { BalenaAPIMock } from '../balena-api-mock'; import { isV13 } from '../../build/utils/version';
import { BalenaAPIMock } from '../nock/balena-api-mock';
import { expectStreamNoCRLF, testDockerBuildStream } from '../docker-build'; import { expectStreamNoCRLF, testDockerBuildStream } from '../docker-build';
import { DockerMock, dockerResponsePath } from '../docker-mock'; import { DockerMock, dockerResponsePath } from '../nock/docker-mock';
import { cleanOutput, runCommand, switchSentry } from '../helpers'; import { cleanOutput, runCommand, switchSentry } from '../helpers';
import { import {
ExpectedTarStreamFiles, ExpectedTarStreamFiles,
@ -50,6 +53,7 @@ const commonResponseLines = {
}; };
const commonQueryParams = [ const commonQueryParams = [
['platform', 'linux/arm/v7'],
['t', '${tag}'], ['t', '${tag}'],
['buildargs', '{}'], ['buildargs', '{}'],
['labels', ''], ['labels', ''],
@ -64,8 +68,10 @@ const commonComposeQueryParams = {
labels: '', labels: '',
}; };
const hr = const commonComposeQueryParamsArmV7 = {
'----------------------------------------------------------------------'; ...commonComposeQueryParams,
platform: 'linux/arm/v7',
};
describe('balena deploy', function () { describe('balena deploy', function () {
let api: BalenaAPIMock; let api: BalenaAPIMock;
@ -77,9 +83,7 @@ describe('balena deploy', function () {
docker = new DockerMock(); docker = new DockerMock();
api.expectGetWhoAmI({ optional: true, persist: true }); api.expectGetWhoAmI({ optional: true, persist: true });
api.expectGetMixpanel({ optional: true }); api.expectGetMixpanel({ optional: true });
api.expectGetConfigDeviceTypes(); api.expectGetApplication({ expandArchitecture: true });
api.expectGetApplication();
api.expectPostRelease();
api.expectGetRelease(); api.expectGetRelease();
api.expectGetUser(); api.expectGetUser();
api.expectGetService({ serviceName: 'main' }); api.expectGetService({ serviceName: 'main' });
@ -137,12 +141,115 @@ describe('balena deploy', function () {
); );
} }
api.expectPostRelease({});
api.expectPatchImage({}); api.expectPatchImage({});
api.expectPatchRelease({}); api.expectPatchRelease({});
api.expectPostImageLabel(); api.expectPostImageLabel();
docker.expectGetManifestBusybox();
await testDockerBuildStream({ await testDockerBuildStream({
commandLine: `deploy testApp --build --source ${projectPath} -G`, commandLine: `deploy testApp --build --source ${projectPath} ${
isV13() ? '' : '-G'
}`,
dockerMock: docker,
expectedFilesByService: { main: expectedFiles },
expectedQueryParamsByService: { main: commonQueryParams },
expectedResponseLines,
projectPath,
responseBody,
responseCode: 200,
services: ['main'],
});
});
it('should handle the contract and final status for a final (non-draft) release', async () => {
const projectPath = path.join(
projectsPath,
'no-docker-compose',
'with-contract',
);
const expectedFiles: ExpectedTarStreamFiles = {
'src/start.sh': { fileSize: 30, type: 'file' },
Dockerfile: { fileSize: 88, type: 'file' },
'balena.yml': { fileSize: 55, type: 'file' },
};
const responseFilename = 'build-POST.json';
const responseBody = await fs.readFile(
path.join(dockerResponsePath, responseFilename),
'utf8',
);
const expectedResponseLines = [
...commonResponseLines[responseFilename],
`[Info] No "docker-compose.yml" file found at "${projectPath}"`,
`[Info] Creating default composition with source: "${projectPath}"`,
];
api.expectPostRelease({
inspectRequest: (_uri: string, requestBody: nock.Body) => {
const body = requestBody.valueOf() as Partial<ReleaseRequest>;
expect(body.contract).to.be.equal(
'{"name":"testContract","type":"sw.application","version":"1.5.2"}',
);
expect(body.is_final).to.be.true;
},
});
api.expectPatchImage({});
api.expectPatchRelease({});
api.expectPostImageLabel();
docker.expectGetManifestBusybox();
await testDockerBuildStream({
commandLine: `deploy testApp --build --source ${projectPath}`,
dockerMock: docker,
expectedFilesByService: { main: expectedFiles },
expectedQueryParamsByService: { main: commonQueryParams },
expectedResponseLines,
projectPath,
responseBody,
responseCode: 200,
services: ['main'],
});
});
it('should handle the contract and final status for a draft release', async () => {
const projectPath = path.join(
projectsPath,
'no-docker-compose',
'with-contract',
);
const expectedFiles: ExpectedTarStreamFiles = {
'src/start.sh': { fileSize: 30, type: 'file' },
Dockerfile: { fileSize: 88, type: 'file' },
'balena.yml': { fileSize: 55, type: 'file' },
};
const responseFilename = 'build-POST.json';
const responseBody = await fs.readFile(
path.join(dockerResponsePath, responseFilename),
'utf8',
);
const expectedResponseLines = [
...commonResponseLines[responseFilename],
`[Info] No "docker-compose.yml" file found at "${projectPath}"`,
`[Info] Creating default composition with source: "${projectPath}"`,
];
api.expectPostRelease({
inspectRequest: (_uri: string, requestBody: nock.Body) => {
const body = requestBody.valueOf() as Partial<ReleaseRequest>;
expect(body.contract).to.be.equal(
'{"name":"testContract","type":"sw.application","version":"1.5.2"}',
);
expect(body.semver).to.be.equal('1.5.2');
expect(body.is_final).to.be.false;
},
});
api.expectPatchImage({});
api.expectPatchRelease({});
api.expectPostImageLabel();
docker.expectGetManifestBusybox();
await testDockerBuildStream({
commandLine: `deploy testApp --build --draft --source ${projectPath}`,
dockerMock: docker, dockerMock: docker,
expectedFilesByService: { main: expectedFiles }, expectedFilesByService: { main: expectedFiles },
expectedQueryParamsByService: { main: commonQueryParams }, expectedQueryParamsByService: { main: commonQueryParams },
@ -176,6 +283,9 @@ describe('balena deploy', function () {
// causes the CLI to call process.exit() with process.exitCode = 1 // causes the CLI to call process.exit() with process.exitCode = 1
const expectedExitCode = 1; const expectedExitCode = 1;
api.expectPostRelease({});
docker.expectGetManifestBusybox();
// Mock this patch HTTP request to return status code 500, in which case // Mock this patch HTTP request to return status code 500, in which case
// the release status should be saved as "failed" rather than "success" // the release status should be saved as "failed" rather than "success"
api.expectPatchImage({ api.expectPatchImage({
@ -204,7 +314,9 @@ describe('balena deploy', function () {
sinon.stub(process, 'exit'); sinon.stub(process, 'exit');
await testDockerBuildStream({ await testDockerBuildStream({
commandLine: `deploy testApp --build --source ${projectPath} --noconvert-eol -G`, commandLine: `deploy testApp --build --source ${projectPath} --noconvert-eol ${
isV13() ? '' : '-G'
}`,
dockerMock: docker, dockerMock: docker,
expectedFilesByService: { main: expectedFiles }, expectedFilesByService: { main: expectedFiles },
expectedQueryParamsByService: { main: commonQueryParams }, expectedQueryParamsByService: { main: commonQueryParams },
@ -250,7 +362,7 @@ describe('balena deploy', function () {
}, },
service2: { service2: {
'.dockerignore': { fileSize: 12, type: 'file' }, '.dockerignore': { fileSize: 12, type: 'file' },
'Dockerfile-alt': { fileSize: 40, type: 'file' }, 'Dockerfile-alt': { fileSize: 13, type: 'file' },
'file2-crlf.sh': { 'file2-crlf.sh': {
fileSize: isWindows ? 12 : 14, fileSize: isWindows ? 12 : 14,
testStream: isWindows ? expectStreamNoCRLF : undefined, testStream: isWindows ? expectStreamNoCRLF : undefined,
@ -270,7 +382,7 @@ describe('balena deploy', function () {
}), }),
), ),
service2: Object.entries( service2: Object.entries(
_.merge({}, commonComposeQueryParams, { _.merge({}, commonComposeQueryParamsArmV7, {
buildargs: { buildargs: {
COMPOSE_ARG: 'an argument defined in the docker-compose.yml file', COMPOSE_ARG: 'an argument defined in the docker-compose.yml file',
}, },
@ -285,11 +397,11 @@ describe('balena deploy', function () {
'[Build] service2 Step 1/4 : FROM busybox', '[Build] service2 Step 1/4 : FROM busybox',
], ],
...[ ...[
`[Info] ${hr}`, `[Info] ---------------------------------------------------------------------------`,
'[Info] The --multi-dockerignore option is being used, and a .dockerignore file was', '[Info] The --multi-dockerignore option is being used, and a .dockerignore file was',
'[Info] found at the project source (root) directory. Note that this file will not', '[Info] found at the project source (root) directory. Note that this file will not',
'[Info] be used to filter service subdirectories. See "balena help deploy".', '[Info] be used to filter service subdirectories. See "balena help deploy".',
`[Info] ${hr}`, `[Info] ---------------------------------------------------------------------------`,
], ],
]; ];
if (isWindows) { if (isWindows) {
@ -302,9 +414,11 @@ describe('balena deploy', function () {
); );
} }
// docker.expectGetImages(); api.expectPostRelease({});
api.expectPatchImage({}); api.expectPatchImage({});
api.expectPatchRelease({}); api.expectPatchRelease({});
docker.expectGetManifestRpi3Alpine();
docker.expectGetManifestBusybox();
await testDockerBuildStream({ await testDockerBuildStream({
commandLine: `deploy testApp --build --source ${projectPath} --multi-dockerignore`, commandLine: `deploy testApp --build --source ${projectPath} --multi-dockerignore`,

View File

@ -16,7 +16,7 @@
*/ */
import { expect } from 'chai'; import { expect } from 'chai';
import { BalenaAPIMock } from '../../balena-api-mock'; import { BalenaAPIMock } from '../../nock/balena-api-mock';
import { cleanOutput, runCommand } from '../../helpers'; import { cleanOutput, runCommand } from '../../helpers';
describe('balena device move', function () { describe('balena device move', function () {

View File

@ -18,7 +18,7 @@
import { expect } from 'chai'; import { expect } from 'chai';
import * as path from 'path'; import * as path from 'path';
import { apiResponsePath, BalenaAPIMock } from '../../balena-api-mock'; import { apiResponsePath, BalenaAPIMock } from '../../nock/balena-api-mock';
import { cleanOutput, runCommand } from '../../helpers'; import { cleanOutput, runCommand } from '../../helpers';
import { appToFleetOutputMsg, warnify } from '../../../build/utils/messages'; import { appToFleetOutputMsg, warnify } from '../../../build/utils/messages';

View File

@ -18,7 +18,7 @@
import { expect } from 'chai'; import { expect } from 'chai';
import * as path from 'path'; import * as path from 'path';
import { apiResponsePath, BalenaAPIMock } from '../../balena-api-mock'; import { apiResponsePath, BalenaAPIMock } from '../../nock/balena-api-mock';
import { cleanOutput, runCommand } from '../../helpers'; import { cleanOutput, runCommand } from '../../helpers';
import { appToFleetOutputMsg, warnify } from '../../../build/utils/messages'; import { appToFleetOutputMsg, warnify } from '../../../build/utils/messages';
@ -59,8 +59,9 @@ describe('balena devices', function () {
const lines = cleanOutput(out); const lines = cleanOutput(out);
expect(lines[0].replace(/ +/g, ' ')).to.equal( expect(lines[0].replace(/ +/g, ' ')).to.equal(
'ID UUID DEVICE NAME DEVICE TYPE APPLICATION NAME STATUS ' + isV13()
'IS ONLINE SUPERVISOR VERSION OS VERSION DASHBOARD URL', ? 'ID UUID DEVICE NAME DEVICE TYPE FLEET STATUS IS ONLINE SUPERVISOR VERSION OS VERSION DASHBOARD URL'
: 'ID UUID DEVICE NAME DEVICE TYPE APPLICATION NAME STATUS IS ONLINE SUPERVISOR VERSION OS VERSION DASHBOARD URL',
); );
expect(lines).to.have.lengthOf.at.least(2); expect(lines).to.have.lengthOf.at.least(2);

View File

@ -17,10 +17,10 @@
import { expect } from 'chai'; import { expect } from 'chai';
import { BalenaAPIMock } from '../../balena-api-mock'; import { BalenaAPIMock } from '../../nock/balena-api-mock';
import { cleanOutput, runCommand } from '../../helpers'; import { cleanOutput, runCommand } from '../../helpers';
import { isV13 } from '../../../lib/utils/version'; import { isV13 } from '../../../build/utils/version';
describe('balena devices supported', function () { describe('balena devices supported', function () {
let api: BalenaAPIMock; let api: BalenaAPIMock;

View File

@ -17,7 +17,7 @@
import { expect } from 'chai'; import { expect } from 'chai';
import { BalenaAPIMock } from '../../balena-api-mock'; import { BalenaAPIMock } from '../../nock/balena-api-mock';
import { runCommand } from '../../helpers'; import { runCommand } from '../../helpers';
describe('balena env add', function () { describe('balena env add', function () {

View File

@ -16,16 +16,12 @@
*/ */
import { expect } from 'chai'; import { expect } from 'chai';
import { stripIndent } from '../../../lib/utils/lazy'; import { stripIndent } from '../../../build/utils/lazy';
import { BalenaAPIMock } from '../../balena-api-mock'; import { BalenaAPIMock } from '../../nock/balena-api-mock';
import { runCommand } from '../../helpers'; import { runCommand } from '../../helpers';
import { import { appToFleetOutputMsg, warnify } from '../../../build/utils/messages';
appToFleetFlagMsg,
appToFleetOutputMsg,
warnify,
} from '../../../build/utils/messages';
import { isV13 } from '../../../build/utils/version'; import { isV13 } from '../../../build/utils/version';
describe('balena envs', function () { describe('balena envs', function () {
@ -48,13 +44,6 @@ describe('balena envs', function () {
api.done(); api.done();
}); });
const appToFleetFlagWarn =
!isV13() &&
process.stderr.isTTY &&
process.env.BALENA_CLI_TEST_TYPE !== 'standalone'
? warnify(appToFleetFlagMsg) + '\n'
: '';
const appToFleetOutputWarn = const appToFleetOutputWarn =
!isV13() && !isV13() &&
process.stderr.isTTY && process.stderr.isTTY &&
@ -67,18 +56,18 @@ describe('balena envs', function () {
api.expectGetAppEnvVars(); api.expectGetAppEnvVars();
api.expectGetAppServiceVars(); api.expectGetAppServiceVars();
const { out, err } = await runCommand(`envs -a ${appName}`); const { out, err } = await runCommand(`envs -f ${appName}`);
expect(out.join('')).to.equal( expect(out.join('')).to.equal(
stripIndent` stripIndent`
ID NAME VALUE APPLICATION SERVICE ID NAME VALUE FLEET SERVICE
120110 svar1 svar1-value test service1 120110 svar1 svar1-value test service1
120111 svar2 svar2-value test service2 120111 svar2 svar2-value test service2
120101 var1 var1-val test * 120101 var1 var1-val test *
120102 var2 22 test * 120102 var2 22 test *
` + '\n', ` + '\n',
); );
expect(err.join('')).to.equal(appToFleetFlagWarn); expect(err.join('')).to.equal('');
}); });
it('should successfully list config vars for a test fleet', async () => { it('should successfully list config vars for a test fleet', async () => {
@ -101,11 +90,11 @@ describe('balena envs', function () {
api.expectGetApplication(); api.expectGetApplication();
api.expectGetAppConfigVars(); api.expectGetAppConfigVars();
const { out, err } = await runCommand(`envs -cja ${appName}`); const { out, err } = await runCommand(`envs -cjf ${appName}`);
expect(JSON.parse(out.join(''))).to.deep.equal([ expect(JSON.parse(out.join(''))).to.deep.equal([
{ {
appName: 'test', fleetName: 'test',
id: 120300, id: 120300,
name: 'RESIN_SUPERVISOR_NATIVE_LOGGER', name: 'RESIN_SUPERVISOR_NATIVE_LOGGER',
value: 'false', value: 'false',
@ -122,18 +111,18 @@ describe('balena envs', function () {
api.expectGetAppServiceVars(); api.expectGetAppServiceVars();
const { out, err } = await runCommand( const { out, err } = await runCommand(
`envs -a ${appName} -s ${serviceName}`, `envs -f ${appName} -s ${serviceName}`,
); );
expect(out.join('')).to.equal( expect(out.join('')).to.equal(
stripIndent` stripIndent`
ID NAME VALUE APPLICATION SERVICE ID NAME VALUE FLEET SERVICE
120111 svar2 svar2-value test service2 120111 svar2 svar2-value test service2
120101 var1 var1-val test * 120101 var1 var1-val test *
120102 var2 22 test * 120102 var2 22 test *
` + '\n', ` + '\n',
); );
expect(err.join('')).to.equal(appToFleetFlagWarn); expect(err.join('')).to.equal('');
}); });
it('should successfully list env and service vars for a test fleet (-s flags)', async () => { it('should successfully list env and service vars for a test fleet (-s flags)', async () => {
@ -144,18 +133,18 @@ describe('balena envs', function () {
api.expectGetAppServiceVars(); api.expectGetAppServiceVars();
const { out, err } = await runCommand( const { out, err } = await runCommand(
`envs -a ${appName} -s ${serviceName}`, `envs -f ${appName} -s ${serviceName}`,
); );
expect(out.join('')).to.equal( expect(out.join('')).to.equal(
stripIndent` stripIndent`
ID NAME VALUE APPLICATION SERVICE ID NAME VALUE FLEET SERVICE
120110 svar1 svar1-value test ${serviceName} 120110 svar1 svar1-value test ${serviceName}
120101 var1 var1-val test * 120101 var1 var1-val test *
120102 var2 22 test * 120102 var2 22 test *
` + '\n', ` + '\n',
); );
expect(err.join('')).to.equal(appToFleetFlagWarn); expect(err.join('')).to.equal('');
}); });
it('should successfully list env variables for a test device', async () => { it('should successfully list env variables for a test device', async () => {
@ -167,22 +156,29 @@ describe('balena envs', function () {
api.expectGetDeviceServiceVars(); api.expectGetDeviceServiceVars();
const uuid = shortUUID; const uuid = shortUUID;
const { out, err } = await runCommand(`envs -d ${uuid}`); const result = await runCommand(`envs -d ${uuid}`);
const { err } = result;
expect(out.join('')).to.equal( let { out } = result;
let expected =
stripIndent` stripIndent`
ID NAME VALUE APPLICATION DEVICE SERVICE ID NAME VALUE APPLICATION DEVICE SERVICE
120110 svar1 svar1-value test * service1 120110 svar1 svar1-value test * service1
120111 svar2 svar2-value test * service2 120111 svar2 svar2-value test * service2
120120 svar3 svar3-value test ${uuid} service1 120120 svar3 svar3-value test ${uuid} service1
120121 svar4 svar4-value test ${uuid} service2 120121 svar4 svar4-value test ${uuid} service2
120101 var1 var1-val test * * 120101 var1 var1-val test * *
120102 var2 22 test * * 120102 var2 22 test * *
120203 var3 var3-val test ${uuid} * 120203 var3 var3-val test ${uuid} *
120204 var4 44 test ${uuid} * 120204 var4 44 test ${uuid} *
` + '\n', ` + '\n';
); if (isV13()) {
out = out.map((l) => l.replace(/ +/g, ' '));
expected = expected
.replace(/ +/g, ' ')
.replace(' APPLICATION ', ' FLEET ')
.replace(/ test /g, ' org/test ');
}
expect(out.join('')).to.equal(expected);
expect(err.join('')).to.equal(appToFleetOutputWarn); expect(err.join('')).to.equal(appToFleetOutputWarn);
}); });
@ -195,9 +191,7 @@ describe('balena envs', function () {
api.expectGetDeviceServiceVars(); api.expectGetDeviceServiceVars();
const { out, err } = await runCommand(`envs -jd ${shortUUID}`); const { out, err } = await runCommand(`envs -jd ${shortUUID}`);
let expected = `[
expect(JSON.parse(out.join(''))).to.deep.equal(
JSON.parse(`[
{ "id": 120101, "appName": "test", "deviceUUID": "*", "name": "var1", "value": "var1-val", "serviceName": "*" }, { "id": 120101, "appName": "test", "deviceUUID": "*", "name": "var1", "value": "var1-val", "serviceName": "*" },
{ "id": 120102, "appName": "test", "deviceUUID": "*", "name": "var2", "value": "22", "serviceName": "*" }, { "id": 120102, "appName": "test", "deviceUUID": "*", "name": "var2", "value": "22", "serviceName": "*" },
{ "id": 120110, "appName": "test", "deviceUUID": "*", "name": "svar1", "value": "svar1-value", "serviceName": "service1" }, { "id": 120110, "appName": "test", "deviceUUID": "*", "name": "svar1", "value": "svar1-value", "serviceName": "service1" },
@ -206,9 +200,14 @@ describe('balena envs', function () {
{ "id": 120121, "appName": "test", "deviceUUID": "${fullUUID}", "name": "svar4", "value": "svar4-value", "serviceName": "service2" }, { "id": 120121, "appName": "test", "deviceUUID": "${fullUUID}", "name": "svar4", "value": "svar4-value", "serviceName": "service2" },
{ "id": 120203, "appName": "test", "deviceUUID": "${fullUUID}", "name": "var3", "value": "var3-val", "serviceName": "*" }, { "id": 120203, "appName": "test", "deviceUUID": "${fullUUID}", "name": "var3", "value": "var3-val", "serviceName": "*" },
{ "id": 120204, "appName": "test", "deviceUUID": "${fullUUID}", "name": "var4", "value": "44", "serviceName": "*" } { "id": 120204, "appName": "test", "deviceUUID": "${fullUUID}", "name": "var4", "value": "44", "serviceName": "*" }
]`), ]`;
); if (isV13()) {
expected = expected.replace(
/"appName": "test"/g,
'"fleetName": "org/test"',
);
}
expect(JSON.parse(out.join(''))).to.deep.equal(JSON.parse(expected));
expect(err.join('')).to.equal(''); expect(err.join('')).to.equal('');
}); });
@ -218,16 +217,23 @@ describe('balena envs', function () {
api.expectGetApplication(); api.expectGetApplication();
api.expectGetAppConfigVars(); api.expectGetAppConfigVars();
const { out, err } = await runCommand(`envs -d ${shortUUID} --config`); const result = await runCommand(`envs -d ${shortUUID} --config`);
const { err } = result;
expect(out.join('')).to.equal( let { out } = result;
let expected =
stripIndent` stripIndent`
ID NAME VALUE APPLICATION DEVICE ID NAME VALUE APPLICATION DEVICE
120300 RESIN_SUPERVISOR_NATIVE_LOGGER false test * 120300 RESIN_SUPERVISOR_NATIVE_LOGGER false test *
120400 RESIN_SUPERVISOR_POLL_INTERVAL 900900 test ${shortUUID} 120400 RESIN_SUPERVISOR_POLL_INTERVAL 900900 test ${shortUUID}
` + '\n', ` + '\n';
); if (isV13()) {
out = out.map((l) => l.replace(/ +/g, ' '));
expected = expected
.replace(/ +/g, ' ')
.replace(' APPLICATION ', ' FLEET ')
.replace(/ test /g, ' org/test ');
}
expect(out.join('')).to.equal(expected);
expect(err.join('')).to.equal(appToFleetOutputWarn); expect(err.join('')).to.equal(appToFleetOutputWarn);
}); });
@ -242,20 +248,27 @@ describe('balena envs', function () {
api.expectGetDeviceEnvVars(); api.expectGetDeviceEnvVars();
const uuid = shortUUID; const uuid = shortUUID;
const { out, err } = await runCommand(`envs -d ${uuid} -s ${serviceName}`); const result = await runCommand(`envs -d ${uuid} -s ${serviceName}`);
const { err } = result;
expect(out.join('')).to.equal( let { out } = result;
let expected =
stripIndent` stripIndent`
ID NAME VALUE APPLICATION DEVICE SERVICE ID NAME VALUE APPLICATION DEVICE SERVICE
120111 svar2 svar2-value test * service2 120111 svar2 svar2-value test * service2
120121 svar4 svar4-value test ${uuid} service2 120121 svar4 svar4-value test ${uuid} service2
120101 var1 var1-val test * * 120101 var1 var1-val test * *
120102 var2 22 test * * 120102 var2 22 test * *
120203 var3 var3-val test ${uuid} * 120203 var3 var3-val test ${uuid} *
120204 var4 44 test ${uuid} * 120204 var4 44 test ${uuid} *
` + '\n', ` + '\n';
); if (isV13()) {
out = out.map((l) => l.replace(/ +/g, ' '));
expected = expected
.replace(/ +/g, ' ')
.replace(' APPLICATION ', ' FLEET ')
.replace(/ test /g, ' org/test ');
}
expect(out.join('')).to.equal(expected);
expect(err.join('')).to.equal(appToFleetOutputWarn); expect(err.join('')).to.equal(appToFleetOutputWarn);
}); });
@ -265,18 +278,24 @@ describe('balena envs', function () {
api.expectGetDeviceServiceVars(); api.expectGetDeviceServiceVars();
const uuid = shortUUID; const uuid = shortUUID;
const result = await runCommand(`envs -d ${uuid}`);
const { out, err } = await runCommand(`envs -d ${uuid}`); const { err } = result;
let { out } = result;
expect(out.join('')).to.equal( let expected =
stripIndent` stripIndent`
ID NAME VALUE APPLICATION DEVICE SERVICE ID NAME VALUE APPLICATION DEVICE SERVICE
120120 svar3 svar3-value N/A ${uuid} service1 120120 svar3 svar3-value N/A ${uuid} service1
120121 svar4 svar4-value N/A ${uuid} service2 120121 svar4 svar4-value N/A ${uuid} service2
120203 var3 var3-val N/A ${uuid} * 120203 var3 var3-val N/A ${uuid} *
120204 var4 44 N/A ${uuid} * 120204 var4 44 N/A ${uuid} *
` + '\n', ` + '\n';
); if (isV13()) {
out = out.map((l) => l.replace(/ +/g, ' '));
expected = expected
.replace(/ +/g, ' ')
.replace(' APPLICATION ', ' FLEET ');
}
expect(out.join('')).to.equal(expected);
expect(err.join('')).to.equal(appToFleetOutputWarn); expect(err.join('')).to.equal(appToFleetOutputWarn);
}); });
@ -291,19 +310,27 @@ describe('balena envs', function () {
api.expectGetDeviceServiceVars(); api.expectGetDeviceServiceVars();
const uuid = shortUUID; const uuid = shortUUID;
const { out, err } = await runCommand(`envs -d ${uuid} -s ${serviceName}`); const result = await runCommand(`envs -d ${uuid} -s ${serviceName}`);
const { err } = result;
expect(out.join('')).to.equal( let { out } = result;
let expected =
stripIndent` stripIndent`
ID NAME VALUE APPLICATION DEVICE SERVICE ID NAME VALUE APPLICATION DEVICE SERVICE
120110 svar1 svar1-value test * ${serviceName} 120110 svar1 svar1-value test * ${serviceName}
120120 svar3 svar3-value test ${uuid} ${serviceName} 120120 svar3 svar3-value test ${uuid} ${serviceName}
120101 var1 var1-val test * * 120101 var1 var1-val test * *
120102 var2 22 test * * 120102 var2 22 test * *
120203 var3 var3-val test ${uuid} * 120203 var3 var3-val test ${uuid} *
120204 var4 44 test ${uuid} * 120204 var4 44 test ${uuid} *
` + '\n', ` + '\n';
); if (isV13()) {
out = out.map((l) => l.replace(/ +/g, ' '));
expected = expected
.replace(/ +/g, ' ')
.replace(' APPLICATION ', ' FLEET ')
.replace(/ test /g, ' org/test ');
}
expect(out.join('')).to.equal(expected);
expect(err.join('')).to.equal(appToFleetOutputWarn); expect(err.join('')).to.equal(appToFleetOutputWarn);
}); });
@ -320,17 +347,21 @@ describe('balena envs', function () {
const { out, err } = await runCommand( const { out, err } = await runCommand(
`envs -d ${shortUUID} -js ${serviceName}`, `envs -d ${shortUUID} -js ${serviceName}`,
); );
let expected = `[
expect(JSON.parse(out.join(''))).to.deep.equal( { "id": 120101, "appName": "test", "deviceUUID": "*", "name": "var1", "value": "var1-val", "serviceName": "*" },
JSON.parse(`[ { "id": 120102, "appName": "test", "deviceUUID": "*", "name": "var2", "value": "22", "serviceName": "*" },
{ "id": 120101, "appName": "test", "deviceUUID": "*", "name": "var1", "value": "var1-val", "serviceName": "*" }, { "id": 120110, "appName": "test", "deviceUUID": "*", "name": "svar1", "value": "svar1-value", "serviceName": "${serviceName}" },
{ "id": 120102, "appName": "test", "deviceUUID": "*", "name": "var2", "value": "22", "serviceName": "*" }, { "id": 120120, "appName": "test", "deviceUUID": "${fullUUID}", "name": "svar3", "value": "svar3-value", "serviceName": "${serviceName}" },
{ "id": 120110, "appName": "test", "deviceUUID": "*", "name": "svar1", "value": "svar1-value", "serviceName": "${serviceName}" }, { "id": 120203, "appName": "test", "deviceUUID": "${fullUUID}", "name": "var3", "value": "var3-val", "serviceName": "*" },
{ "id": 120120, "appName": "test", "deviceUUID": "${fullUUID}", "name": "svar3", "value": "svar3-value", "serviceName": "${serviceName}" }, { "id": 120204, "appName": "test", "deviceUUID": "${fullUUID}", "name": "var4", "value": "44", "serviceName": "*" }
{ "id": 120203, "appName": "test", "deviceUUID": "${fullUUID}", "name": "var3", "value": "var3-val", "serviceName": "*" }, ]`;
{ "id": 120204, "appName": "test", "deviceUUID": "${fullUUID}", "name": "var4", "value": "44", "serviceName": "*" } if (isV13()) {
]`), expected = expected.replace(
); /"appName": "test"/g,
'"fleetName": "org/test"',
);
}
expect(JSON.parse(out.join(''))).to.deep.equal(JSON.parse(expected));
expect(err.join('')).to.equal(''); expect(err.join('')).to.equal('');
}); });
}); });

View File

@ -17,7 +17,7 @@
import { expect } from 'chai'; import { expect } from 'chai';
import { BalenaAPIMock } from '../../balena-api-mock'; import { BalenaAPIMock } from '../../nock/balena-api-mock';
import { runCommand } from '../../helpers'; import { runCommand } from '../../helpers';
describe('balena env rename', function () { describe('balena env rename', function () {

View File

@ -17,7 +17,7 @@
import { expect } from 'chai'; import { expect } from 'chai';
import { BalenaAPIMock } from '../../balena-api-mock'; import { BalenaAPIMock } from '../../nock/balena-api-mock';
import { runCommand } from '../../helpers'; import { runCommand } from '../../helpers';
describe('balena env rm', function () { describe('balena env rm', function () {

View File

@ -17,7 +17,7 @@
import { expect } from 'chai'; import { expect } from 'chai';
import { BalenaAPIMock } from '../balena-api-mock'; import { BalenaAPIMock } from '../nock/balena-api-mock';
import { cleanOutput, runCommand } from '../helpers'; import { cleanOutput, runCommand } from '../helpers';
import * as messages from '../../build/utils/messages'; import * as messages from '../../build/utils/messages';

View File

@ -17,9 +17,9 @@
import { expect } from 'chai'; import { expect } from 'chai';
import { BalenaAPIMock } from '../balena-api-mock'; import { BalenaAPIMock } from '../nock/balena-api-mock';
import { cleanOutput, runCommand } from '../helpers'; import { cleanOutput, runCommand } from '../helpers';
import { SupervisorMock } from '../supervisor-mock'; import { SupervisorMock } from '../nock/supervisor-mock';
const itS = process.env.BALENA_CLI_TEST_TYPE === 'standalone' ? it : it.skip; const itS = process.env.BALENA_CLI_TEST_TYPE === 'standalone' ? it : it.skip;

View File

@ -25,7 +25,7 @@ import * as tmp from 'tmp';
tmp.setGracefulCleanup(); tmp.setGracefulCleanup();
const tmpNameAsync = promisify(tmp.tmpName); const tmpNameAsync = promisify(tmp.tmpName);
import { BalenaAPIMock } from '../../balena-api-mock'; import { BalenaAPIMock } from '../../nock/balena-api-mock';
if (process.platform !== 'win32') { if (process.platform !== 'win32') {
describe('balena os configure', function () { describe('balena os configure', function () {

View File

@ -19,8 +19,9 @@ import { expect } from 'chai';
import { promises as fs } from 'fs'; import { promises as fs } from 'fs';
import * as path from 'path'; import * as path from 'path';
import { BalenaAPIMock } from '../balena-api-mock'; import { isV13 } from '../../build/utils/version';
import { BuilderMock, builderResponsePath } from '../builder-mock'; import { BalenaAPIMock } from '../nock/balena-api-mock';
import { BuilderMock, builderResponsePath } from '../nock/builder-mock';
import { expectStreamNoCRLF, testPushBuildStream } from '../docker-build'; import { expectStreamNoCRLF, testPushBuildStream } from '../docker-build';
import { cleanOutput, runCommand } from '../helpers'; import { cleanOutput, runCommand } from '../helpers';
import { import {
@ -34,6 +35,8 @@ import {
const repoPath = path.normalize(path.join(__dirname, '..', '..')); const repoPath = path.normalize(path.join(__dirname, '..', '..'));
const projectsPath = path.join(repoPath, 'tests', 'test-data', 'projects'); const projectsPath = path.join(repoPath, 'tests', 'test-data', 'projects');
const itNoV13 = isV13() ? it.skip : it;
const commonResponseLines = { const commonResponseLines = {
'build-POST-v3.json': [ 'build-POST-v3.json': [
'[Info] Starting build for testApp, user gh_user', '[Info] Starting build for testApp, user gh_user',
@ -73,6 +76,7 @@ const commonQueryParams = [
['emulated', 'false'], ['emulated', 'false'],
['nocache', 'false'], ['nocache', 'false'],
['headless', 'false'], ['headless', 'false'],
['isdraft', 'false'],
]; ];
const hr = const hr =
@ -233,71 +237,74 @@ describe('balena push', function () {
}); });
}); });
it('should create the expected tar stream (single container, --gitignore)', async () => { itNoV13(
const projectPath = path.join( 'should create the expected tar stream (single container, --gitignore)',
projectsPath, async () => {
'no-docker-compose', const projectPath = path.join(
'dockerignore1', projectsPath,
); 'no-docker-compose',
const expectedFiles: ExpectedTarStreamFiles = { 'dockerignore1',
'.balena/balena.yml': { fileSize: 12, type: 'file' }, );
'.dockerignore': { fileSize: 438, type: 'file' }, const expectedFiles: ExpectedTarStreamFiles = {
'.gitignore': { fileSize: 20, type: 'file' }, '.balena/balena.yml': { fileSize: 12, type: 'file' },
'.git/bar.txt': { fileSize: 4, type: 'file' }, '.dockerignore': { fileSize: 438, type: 'file' },
'.git/foo.txt': { fileSize: 4, type: 'file' }, '.gitignore': { fileSize: 20, type: 'file' },
'c.txt': { fileSize: 1, type: 'file' }, '.git/bar.txt': { fileSize: 4, type: 'file' },
Dockerfile: { fileSize: 13, type: 'file' }, '.git/foo.txt': { fileSize: 4, type: 'file' },
'src/.balena/balena.yml': { fileSize: 16, type: 'file' }, 'c.txt': { fileSize: 1, type: 'file' },
'src/.gitignore': { fileSize: 10, type: 'file' }, Dockerfile: { fileSize: 13, type: 'file' },
'vendor/.git/vendor-git-contents': { fileSize: 20, type: 'file' }, 'src/.balena/balena.yml': { fileSize: 16, type: 'file' },
// When --gitignore (-g) is provided for v11 compatibility, the old 'src/.gitignore': { fileSize: 10, type: 'file' },
// `zeit/dockerignore` npm package is still used but it is broken on 'vendor/.git/vendor-git-contents': { fileSize: 20, type: 'file' },
// Windows (reason why we created `@balena/dockerignore`). // When --gitignore (-g) is provided for v11 compatibility, the old
...(isWindows // `zeit/dockerignore` npm package is still used but it is broken on
? { // Windows (reason why we created `@balena/dockerignore`).
'src/src-b.txt': { fileSize: 5, type: 'file' }, ...(isWindows
'dot.git/bar.txt': { fileSize: 4, type: 'file' }, ? {
'dot.git/foo.txt': { fileSize: 4, type: 'file' }, 'src/src-b.txt': { fileSize: 5, type: 'file' },
'vendor/dot.git/vendor-git-contents': { 'dot.git/bar.txt': { fileSize: 4, type: 'file' },
fileSize: 20, 'dot.git/foo.txt': { fileSize: 4, type: 'file' },
type: 'file', 'vendor/dot.git/vendor-git-contents': {
}, fileSize: 20,
} type: 'file',
: {}), },
}; }
: {}),
};
const regSecretsPath = await addRegSecretsEntries(expectedFiles); const regSecretsPath = await addRegSecretsEntries(expectedFiles);
const responseFilename = 'build-POST-v3.json'; const responseFilename = 'build-POST-v3.json';
const responseBody = await fs.readFile( const responseBody = await fs.readFile(
path.join(builderResponsePath, responseFilename), path.join(builderResponsePath, responseFilename),
'utf8', 'utf8',
); );
const expectedResponseLines = [ const expectedResponseLines = [
...[ ...[
`[Warn] ${hr}`, `[Warn] ${hr}`,
'[Warn] Using file ignore patterns from:', '[Warn] Using file ignore patterns from:',
`[Warn] * ${path.join(projectPath, '.dockerignore')}`, `[Warn] * ${path.join(projectPath, '.dockerignore')}`,
`[Warn] * ${path.join(projectPath, '.gitignore')}`, `[Warn] * ${path.join(projectPath, '.gitignore')}`,
`[Warn] * ${path.join(projectPath, 'src', '.gitignore')}`, `[Warn] * ${path.join(projectPath, 'src', '.gitignore')}`,
'[Warn] .gitignore files are being considered because the --gitignore option was used.', '[Warn] .gitignore files are being considered because the --gitignore option was used.',
'[Warn] This option is deprecated and will be removed in the next major version release.', '[Warn] This option is deprecated and will be removed in the next major version release.',
"[Warn] For more information, see 'balena help push'.", "[Warn] For more information, see 'balena help push'.",
`[Warn] ${hr}`, `[Warn] ${hr}`,
], ],
...commonResponseLines[responseFilename], ...commonResponseLines[responseFilename],
]; ];
await testPushBuildStream({ await testPushBuildStream({
builderMock: builder, builderMock: builder,
commandLine: `push testApp -s ${projectPath} -R ${regSecretsPath} -g`, commandLine: `push testApp -s ${projectPath} -R ${regSecretsPath} -g`,
expectedFiles, expectedFiles,
expectedQueryParams: commonQueryParams, expectedQueryParams: commonQueryParams,
expectedResponseLines, expectedResponseLines,
projectPath, projectPath,
responseBody, responseBody,
responseCode: 200, responseCode: 200,
}); });
}); },
);
it('should create the expected tar stream (single container, --nogitignore)', async () => { it('should create the expected tar stream (single container, --nogitignore)', async () => {
const projectPath = path.join( const projectPath = path.join(
@ -369,24 +376,27 @@ describe('balena push', function () {
path.join(builderResponsePath, responseFilename), path.join(builderResponsePath, responseFilename),
'utf8', 'utf8',
); );
const expectedResponseLines = isWindows const expectedResponseLines =
? [ !isV13() && isWindows
`[Warn] ${hr}`, ? [
'[Warn] Using file ignore patterns from:', `[Warn] ${hr}`,
`[Warn] * ${path.join(projectPath, '.dockerignore')}`, '[Warn] Using file ignore patterns from:',
'[Warn] The --gitignore option was used, but no .gitignore files were found.', `[Warn] * ${path.join(projectPath, '.dockerignore')}`,
'[Warn] The --gitignore option is deprecated and will be removed in the next major', '[Warn] The --gitignore option was used, but no .gitignore files were found.',
'[Warn] version release. It prevents the use of a better dockerignore parser and', '[Warn] The --gitignore option is deprecated and will be removed in the next major',
'[Warn] filter library that fixes several issues on Windows and improves compatibility', '[Warn] version release. It prevents the use of a better dockerignore parser and',
"[Warn] with 'docker build'. For more information, see 'balena help push'.", '[Warn] filter library that fixes several issues on Windows and improves compatibility',
`[Warn] ${hr}`, "[Warn] with 'docker build'. For more information, see 'balena help push'.",
...commonResponseLines[responseFilename], `[Warn] ${hr}`,
] ...commonResponseLines[responseFilename],
: commonResponseLines[responseFilename]; ]
: commonResponseLines[responseFilename];
await testPushBuildStream({ await testPushBuildStream({
builderMock: builder, builderMock: builder,
commandLine: `push testApp -s ${projectPath} -R ${regSecretsPath} --gitignore`, commandLine: `push testApp -s ${projectPath} -R ${regSecretsPath} ${
isV13() ? '' : '--gitignore'
}`,
expectedFiles, expectedFiles,
expectedQueryParams: commonQueryParams, expectedQueryParams: commonQueryParams,
expectedResponseLines, expectedResponseLines,
@ -445,7 +455,7 @@ describe('balena push', function () {
'docker-compose.yml': { fileSize: 332, type: 'file' }, 'docker-compose.yml': { fileSize: 332, type: 'file' },
'service1/Dockerfile.template': { fileSize: 144, type: 'file' }, 'service1/Dockerfile.template': { fileSize: 144, type: 'file' },
'service1/file1.sh': { fileSize: 12, type: 'file' }, 'service1/file1.sh': { fileSize: 12, type: 'file' },
'service2/Dockerfile-alt': { fileSize: 40, type: 'file' }, 'service2/Dockerfile-alt': { fileSize: 13, type: 'file' },
'service2/.dockerignore': { fileSize: 12, type: 'file' }, 'service2/.dockerignore': { fileSize: 12, type: 'file' },
'service2/file2-crlf.sh': { 'service2/file2-crlf.sh': {
fileSize: isWindows ? 12 : 14, fileSize: isWindows ? 12 : 14,
@ -498,7 +508,7 @@ describe('balena push', function () {
'service1/Dockerfile.template': { fileSize: 144, type: 'file' }, 'service1/Dockerfile.template': { fileSize: 144, type: 'file' },
'service1/file1.sh': { fileSize: 12, type: 'file' }, 'service1/file1.sh': { fileSize: 12, type: 'file' },
'service1/test-ignore.txt': { fileSize: 12, type: 'file' }, 'service1/test-ignore.txt': { fileSize: 12, type: 'file' },
'service2/Dockerfile-alt': { fileSize: 40, type: 'file' }, 'service2/Dockerfile-alt': { fileSize: 13, type: 'file' },
'service2/.dockerignore': { fileSize: 12, type: 'file' }, 'service2/.dockerignore': { fileSize: 12, type: 'file' },
'service2/file2-crlf.sh': { 'service2/file2-crlf.sh': {
fileSize: isWindows ? 12 : 14, fileSize: isWindows ? 12 : 14,
@ -515,11 +525,11 @@ describe('balena push', function () {
const expectedResponseLines: string[] = [ const expectedResponseLines: string[] = [
...commonResponseLines[responseFilename], ...commonResponseLines[responseFilename],
...[ ...[
`[Info] ${hr}`, `[Info] ---------------------------------------------------------------------------`,
'[Info] The --multi-dockerignore option is being used, and a .dockerignore file was', '[Info] The --multi-dockerignore option is being used, and a .dockerignore file was',
'[Info] found at the project source (root) directory. Note that this file will not', '[Info] found at the project source (root) directory. Note that this file will not',
'[Info] be used to filter service subdirectories. See "balena help push".', '[Info] be used to filter service subdirectories. See "balena help push".',
`[Info] ${hr}`, `[Info] ---------------------------------------------------------------------------`,
], ],
]; ];
if (isWindows) { if (isWindows) {
@ -570,7 +580,7 @@ describe('balena push: project validation', function () {
]; ];
const { out, err } = await runCommand( const { out, err } = await runCommand(
`push testApp --source ${projectPath} --gitignore`, `push testApp --source ${projectPath}`,
); );
expect(cleanOutput(err, true)).to.include.members(expectedErrorLines); expect(cleanOutput(err, true)).to.include.members(expectedErrorLines);
expect(out).to.be.empty; expect(out).to.be.empty;

View File

@ -0,0 +1,68 @@
/**
* @license
* Copyright 2019-2020 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.
*/
import { expect } from 'chai';
import { BalenaAPIMock } from '../nock/balena-api-mock';
import { cleanOutput, runCommand } from '../helpers';
describe('balena release', function () {
let api: BalenaAPIMock;
beforeEach(() => {
api = new BalenaAPIMock();
api.expectGetWhoAmI({ optional: true, persist: true });
api.expectGetMixpanel({ optional: true });
});
afterEach(() => {
// Check all expected api calls have been made and clean up.
api.done();
});
it('should show release details', async () => {
api.expectGetRelease();
const { out } = await runCommand('release 27fda508c');
const lines = cleanOutput(out);
expect(lines[0]).to.contain('ID: ');
expect(lines[0]).to.contain(' 142334');
expect(lines[1]).to.contain('COMMIT: ');
expect(lines[1]).to.contain(' 90247b54de4fa7a0a3cbc85e73c68039');
});
it('should return release composition', async () => {
api.expectGetRelease();
const { out } = await runCommand('release 27fda508c --composition');
const lines = cleanOutput(out);
expect(lines[0]).to.be.equal("version: '2.1'");
expect(lines[1]).to.be.equal('networks: {}');
expect(lines[2]).to.be.equal('volumes:');
expect(lines[3]).to.be.equal('resin-data: {}');
expect(lines[4]).to.be.equal('services:');
expect(lines[5]).to.be.equal('main:');
});
it('should list releases', async () => {
api.expectGetRelease();
api.expectGetApplication();
const { out } = await runCommand('releases someapp');
const lines = cleanOutput(out);
expect(lines.length).to.be.equal(2);
expect(lines[1]).to.contain('142334');
expect(lines[1]).to.contain('90247b54de4fa7a0a3cbc85e73c68039');
});
});

View File

@ -19,7 +19,7 @@ import { expect } from 'chai';
import mock = require('mock-require'); import mock = require('mock-require');
import { createServer, Server } from 'net'; import { createServer, Server } from 'net';
import { BalenaAPIMock } from '../balena-api-mock'; import { BalenaAPIMock } from '../nock/balena-api-mock';
import { cleanOutput, runCommand } from '../helpers'; import { cleanOutput, runCommand } from '../helpers';
// "itSS" means "it() Skip Standalone" // "itSS" means "it() Skip Standalone"

View File

@ -17,7 +17,7 @@
import { expect } from 'chai'; import { expect } from 'chai';
import * as fs from 'fs'; import * as fs from 'fs';
import { BalenaAPIMock } from '../balena-api-mock'; import { BalenaAPIMock } from '../nock/balena-api-mock';
import { runCommand } from '../helpers'; import { runCommand } from '../helpers';
const packageJSON = JSON.parse(fs.readFileSync('./package.json', 'utf8')); const packageJSON = JSON.parse(fs.readFileSync('./package.json', 'utf8'));

View File

@ -19,10 +19,20 @@ import { set as setEsVersion } from '@balena/es-version';
// Set the desired es version for downstream modules that support it // Set the desired es version for downstream modules that support it
setEsVersion('es2018'); setEsVersion('es2018');
// Disable Sentry.io error reporting while running test code
process.env.BALENARC_NO_SENTRY = '1';
// Disable deprecation checks while running test code
// Like the global `--unsupported` flag
process.env.BALENARC_UNSUPPORTED = '1';
import * as tmp from 'tmp'; import * as tmp from 'tmp';
tmp.setGracefulCleanup(); tmp.setGracefulCleanup();
// Use a temporary dir for tests data // Use a temporary dir for tests data
process.env.BALENARC_DATA_DIRECTORY = tmp.dirSync().name; process.env.BALENARC_DATA_DIRECTORY = tmp.dirSync().name;
console.error(
`[debug] tests/config-tests.ts: BALENARC_DATA_DIRECTORY="${process.env.BALENARC_DATA_DIRECTORY}"`,
);
import { EventEmitter } from 'events'; import { EventEmitter } from 'events';
EventEmitter.defaultMaxListeners = 35; // it appears that 'nock' adds a bunch of listeners - bug? EventEmitter.defaultMaxListeners = 35; // it appears that 'nock' adds a bunch of listeners - bug?
@ -33,3 +43,7 @@ import { config as chaiCfg } from 'chai';
chaiCfg.showDiff = true; chaiCfg.showDiff = true;
// enable diff comparison of large objects / arrays // enable diff comparison of large objects / arrays
chaiCfg.truncateThreshold = 0; chaiCfg.truncateThreshold = 0;
// Because mocks are pointed at "production", we need to make sure this is set to prod.
// Otherwise if the user has BALENARC_BALENA_URL pointing at something else like staging, tests
// will fail.
process.env.BALENARC_BALENA_URL = 'balena-cloud.com';

326
tests/deprecation.spec.ts Normal file
View File

@ -0,0 +1,326 @@
/**
* @license
* Copyright 2021 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.
*/
import * as settings from 'balena-settings-client';
import * as getStorage from 'balena-settings-storage';
import { expect } from 'chai';
import mock = require('mock-require');
import * as semver from 'semver';
import * as sinon from 'sinon';
import * as packageJSON from '../package.json';
import {
DeprecationChecker,
ReleaseTimestampsByVersion,
} from '../build/deprecation';
import { BalenaAPIMock } from './nock/balena-api-mock';
import { NpmMock } from './nock/npm-mock';
import { runCommand, TestOutput } from './helpers';
// "itSS" means "it() Skip Standalone"
const itSS = process.env.BALENA_CLI_TEST_TYPE === 'standalone' ? it.skip : it;
describe('DeprecationChecker', function () {
const sandbox = sinon.createSandbox();
const now = new Date().getTime();
const anHourAgo = now - 3600000;
const currentMajor = semver.major(packageJSON.version, { loose: true });
const nextMajorVersion = `${currentMajor + 1}.0.0`;
const dataDirectory = settings.get<string>('dataDirectory');
const storageModPath = 'balena-settings-storage';
const mockStorage = getStorage({ dataDirectory });
let api: BalenaAPIMock;
let npm: NpmMock;
let checker: DeprecationChecker;
let getStub: sinon.SinonStub<
Parameters<typeof mockStorage.get>,
ReturnType<typeof mockStorage.get>
>;
let setStub: sinon.SinonStub<
Parameters<typeof mockStorage.set>,
ReturnType<typeof mockStorage.set>
>;
let originalUnsupported: string | undefined;
this.beforeAll(() => {
// Temporarily undo settings from `tests/config-tests.ts`
originalUnsupported = process.env.BALENARC_UNSUPPORTED;
delete process.env.BALENARC_UNSUPPORTED;
});
this.afterAll(() => {
process.env.BALENARC_UNSUPPORTED = originalUnsupported;
});
this.beforeEach(() => {
npm = new NpmMock();
api = new BalenaAPIMock();
api.expectGetWhoAmI({ optional: true, persist: true });
api.expectGetMixpanel({ optional: true });
checker = new DeprecationChecker(packageJSON.version);
getStub = sandbox.stub(mockStorage, 'get').withArgs(checker.cacheFile);
setStub = sandbox
.stub(mockStorage, 'set')
.withArgs(checker.cacheFile, sinon.match.any);
mock(storageModPath, () => mockStorage);
});
this.afterEach(() => {
// Check all expected api calls have been made and clean up.
(mockStorage.get as sinon.SinonStub).restore();
(mockStorage.set as sinon.SinonStub).restore();
// originalStorage.set.restore();
api.done();
npm.done();
mock.stop(storageModPath);
});
itSS(
'should warn if this version of the CLI is deprecated (isTTY is true)',
async () => {
const mockCache: ReleaseTimestampsByVersion = {
lastFetched: '1970-01-01T00:00:00.000Z',
};
// pretend the next major was released just over half a year ago
mockCache[nextMajorVersion] = new Date(
checker.now - (checker.deprecationDays + 1) * checker.msInDay,
).toISOString();
getStub.resolves(mockCache);
// Force isTTY to be true. It happens to be false (undefined) when
// the tests run on balenaCI on Windows.
const originalIsTTY = process.stderr.isTTY;
process.stderr.isTTY = true;
let result: TestOutput;
try {
result = await runCommand('version');
} finally {
process.stderr.isTTY = originalIsTTY;
}
const { out, err } = result;
expect(setStub.callCount).to.equal(0);
expect(getStub.callCount).to.equal(1);
expect(getStub.firstCall.args).to.deep.equal([checker.cacheFile]);
expect(out.join('')).to.equal(packageJSON.version + '\n');
expect(err.join('')).to.equal(
checker.getDeprecationMsg(checker.deprecationDays + 1) + '\n',
);
},
);
itSS(
'should NOT warn if this version of the CLI is deprecated (isTTY is false)',
async () => {
const mockCache: ReleaseTimestampsByVersion = {
lastFetched: '1970-01-01T00:00:00.000Z',
};
// pretend the next major was released just over half a year ago
mockCache[nextMajorVersion] = new Date(
checker.now - (checker.deprecationDays + 1) * checker.msInDay,
).toISOString();
getStub.resolves(mockCache);
// Force isTTY to be false (undefined). It happens to be true when
// the tests run on balenaCI on macOS and Linux.
const originalIsTTY = process.stderr.isTTY;
process.stderr.isTTY = undefined;
let result: TestOutput;
try {
result = await runCommand('version');
} finally {
process.stderr.isTTY = originalIsTTY;
}
const { out, err } = result;
expect(setStub.callCount).to.equal(0);
expect(getStub.callCount).to.equal(1);
expect(getStub.firstCall.args).to.deep.equal([checker.cacheFile]);
expect(out.join('')).to.equal(packageJSON.version + '\n');
expect(err.join('')).to.equal('');
},
);
itSS(
'should NOT warn with --unsupported (deprecated but not expired)',
async () => {
const mockCache: ReleaseTimestampsByVersion = {
lastFetched: '1970-01-01T00:00:00.000Z',
};
// pretend the next major was released just over half a year ago
mockCache[nextMajorVersion] = new Date(
checker.now - (checker.deprecationDays + 1) * checker.msInDay,
).toISOString();
getStub.resolves(mockCache);
const { out, err } = await runCommand('version --unsupported');
expect(setStub.callCount).to.equal(0);
expect(getStub.callCount).to.equal(0);
expect(out.join('')).to.equal(packageJSON.version + '\n');
expect(err.join('')).to.be.empty;
},
);
itSS('should exit if this version of the CLI has expired', async () => {
const mockCache: ReleaseTimestampsByVersion = {
lastFetched: '1970-01-01T00:00:00.000Z',
};
// pretend the next major was released just over a year ago
mockCache[nextMajorVersion] = new Date(
checker.now - (checker.expiryDays + 1) * checker.msInDay,
).toISOString();
getStub.resolves(mockCache);
const { out, err } = await runCommand('version');
expect(setStub.callCount).to.equal(0);
expect(getStub.callCount).to.equal(1);
expect(getStub.firstCall.args).to.deep.equal([checker.cacheFile]);
expect(out.join('')).to.equal('');
expect(err.join('')).to.include(
checker.getExpiryMsg(checker.expiryDays + 1) + '\n\n',
);
});
itSS('should NOT exit with --unsupported (expired version)', async () => {
const mockCache: ReleaseTimestampsByVersion = {
lastFetched: '1970-01-01T00:00:00.000Z',
};
// pretend the next major was released just over a year ago
mockCache[nextMajorVersion] = new Date(
checker.now - (checker.expiryDays + 1) * checker.msInDay,
).toISOString();
getStub.resolves(mockCache);
const { out, err } = await runCommand('--unsupported version');
expect(setStub.callCount).to.equal(0);
expect(getStub.callCount).to.equal(0);
expect(out.join('')).to.equal(packageJSON.version + '\n');
expect(err.join('')).to.be.empty;
});
it('should query the npm registry (empty cache file)', async () => {
npm.expectGetBalenaCli({
version: nextMajorVersion,
publishedAt: new Date().toISOString(),
});
getStub.resolves(undefined);
const { out, err } = await runCommand('version');
expect(setStub.callCount).to.equal(1);
expect(setStub.firstCall.args.length).to.equal(2);
const [name, obj] = setStub.firstCall.args;
expect(name).to.equal(checker.cacheFile);
expect(obj).to.have.property(nextMajorVersion);
const lastFetched = new Date(obj[nextMajorVersion]).getTime();
expect(lastFetched).to.be.greaterThan(anHourAgo);
expect(getStub.callCount).to.equal(1);
expect(getStub.firstCall.args).to.deep.equal([checker.cacheFile]);
expect(out.join('')).to.equal(packageJSON.version + '\n');
expect(err.join('')).to.equal('');
});
it('should query the npm registry (not recently fetched)', async () => {
npm.expectGetBalenaCli({
version: nextMajorVersion,
publishedAt: new Date().toISOString(),
});
const mockCache: ReleaseTimestampsByVersion = {
lastFetched: new Date(
checker.now -
(checker.majorVersionFetchIntervalDays + 1) * checker.msInDay,
).toISOString(),
};
getStub.resolves(mockCache);
const { out, err } = await runCommand('version');
expect(setStub.callCount).to.equal(1);
expect(setStub.firstCall.args.length).to.equal(2);
const [name, obj] = setStub.firstCall.args;
expect(name).to.equal(checker.cacheFile);
expect(obj).to.have.property(nextMajorVersion);
const lastFetched = new Date(obj[nextMajorVersion]).getTime();
expect(lastFetched).to.be.greaterThan(anHourAgo);
expect(getStub.callCount).to.equal(1);
expect(getStub.firstCall.args).to.deep.equal([checker.cacheFile]);
expect(out.join('')).to.equal(packageJSON.version + '\n');
expect(err.join('')).to.equal('');
});
itSS('should NOT query the npm registry (recently fetched)', async () => {
const mockCache: ReleaseTimestampsByVersion = {
lastFetched: new Date(
checker.now -
(checker.majorVersionFetchIntervalDays - 1) * checker.msInDay,
).toISOString(),
};
getStub.resolves(mockCache);
const { out, err } = await runCommand('version');
expect(setStub.callCount).to.equal(0);
expect(getStub.callCount).to.equal(1);
expect(getStub.firstCall.args).to.deep.equal([checker.cacheFile]);
expect(out.join('')).to.equal(packageJSON.version + '\n');
expect(err.join('')).to.equal('');
});
itSS('should NOT query the npm registry (cached value)', async () => {
const mockCache: ReleaseTimestampsByVersion = {
lastFetched: '1970-01-01T00:00:00.000Z',
};
// pretend the next major was released just under half a year ago
mockCache[nextMajorVersion] = new Date(
checker.now - (checker.deprecationDays - 1) * checker.msInDay,
).toISOString();
getStub.resolves(mockCache);
const { out, err } = await runCommand('version');
expect(setStub.callCount).to.equal(0);
expect(getStub.callCount).to.equal(1);
expect(getStub.firstCall.args).to.deep.equal([checker.cacheFile]);
expect(out.join('')).to.equal(packageJSON.version + '\n');
expect(err.join('')).to.equal('');
});
});

View File

@ -27,9 +27,10 @@ import * as tar from 'tar-stream';
import { streamToBuffer } from 'tar-utils'; import { streamToBuffer } from 'tar-utils';
import { URL } from 'url'; import { URL } from 'url';
import { stripIndent } from '../lib/utils/lazy'; import { makeImageName } from '../build/utils/compose_ts';
import { BuilderMock } from './builder-mock'; import { stripIndent } from '../build/utils/lazy';
import { DockerMock } from './docker-mock'; import { BuilderMock } from './nock/builder-mock';
import { DockerMock } from './nock/docker-mock';
import { import {
cleanOutput, cleanOutput,
deepJsonParse, deepJsonParse,
@ -161,22 +162,24 @@ export async function testDockerBuildStream(o: {
expectedErrorLines?: string[]; expectedErrorLines?: string[];
expectedExitCode?: number; expectedExitCode?: number;
expectedResponseLines: string[]; expectedResponseLines: string[];
projectName?: string; // --projectName command line flag
projectPath: string; projectPath: string;
responseCode: number; responseCode: number;
responseBody: string; responseBody: string;
services: string[]; // e.g. ['main'] or ['service1', 'service2'] services: string[]; // e.g. ['main'] or ['service1', 'service2']
tag?: string; // --tag command line flag
}) { }) {
const expectedErrorLines = deepTemplateReplace(o.expectedErrorLines || [], o); const expectedErrorLines = deepTemplateReplace(o.expectedErrorLines || [], o);
const expectedResponseLines = deepTemplateReplace(o.expectedResponseLines, o); const expectedResponseLines = deepTemplateReplace(o.expectedResponseLines, o);
for (const service of o.services) { for (const service of o.services) {
// tagPrefix is, for example, 'myApp' if the path is 'path/to/myApp' // tagPrefix is, for example, 'myApp' if the path is 'path/to/myApp'
const tagPrefix = o.projectPath.split(path.sep).pop(); const projectName = o.projectName || path.basename(o.projectPath);
const tag = `${tagPrefix}_${service}`; const tag = makeImageName(projectName, service, o.tag);
const expectedFiles = o.expectedFilesByService[service]; const expectedFiles = o.expectedFilesByService[service];
const expectedQueryParams = deepTemplateReplace( const expectedQueryParams = deepTemplateReplace(
o.expectedQueryParamsByService[service], o.expectedQueryParamsByService[service],
{ tag, ...o }, { ...o, tag },
); );
const projectPath = const projectPath =
service === 'main' ? o.projectPath : path.join(o.projectPath, service); service === 'main' ? o.projectPath : path.join(o.projectPath, service);
@ -195,7 +198,7 @@ export async function testDockerBuildStream(o: {
tag, tag,
}); });
if (o.commandLine.startsWith('build')) { if (o.commandLine.startsWith('build')) {
o.dockerMock.expectGetImages(); o.dockerMock.expectGetImages({ optional: true });
} }
} }

View File

@ -1,6 +1,6 @@
/** /**
* @license * @license
* Copyright 2019-2020 Balena Ltd. * Copyright 2019-2021 Balena Ltd.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -15,36 +15,59 @@
* limitations under the License. * limitations under the License.
*/ */
import { execFile } from 'child_process';
import intercept = require('intercept-stdout');
import * as _ from 'lodash'; import * as _ from 'lodash';
import { promises as fs } from 'fs';
import * as nock from 'nock';
import * as path from 'path'; import * as path from 'path';
import * as balenaCLI from '../build/app'; import * as packageJSON from '../package.json';
const balenaExe = process.platform === 'win32' ? 'balena.exe' : 'balena'; const balenaExe = process.platform === 'win32' ? 'balena.exe' : 'balena';
const standalonePath = path.resolve(__dirname, '..', 'build-bin', balenaExe); const standalonePath = path.resolve(__dirname, '..', 'build-bin', balenaExe);
interface TestOutput { export interface TestOutput {
err: string[]; // stderr err: string[]; // stderr
out: string[]; // stdout out: string[]; // stdout
exitCode?: number; // process.exitCode exitCode?: number; // process.exitCode
} }
function matchesNodeEngineVersionWarn(msg: string) {
if (/^-----+\r?\n?$/.test(msg)) {
return true;
}
const cleanup = (line: string): string[] =>
line
.replace(/-----+/g, '')
.replace(/"\d+\.\d+\.\d+"/, '"x.y.z"')
.split(/\r?\n/)
.map((l) => l.trim())
.filter((l) => l);
const { getNodeEngineVersionWarn } = require('../build/utils/messages');
let nodeEngineWarn: string = getNodeEngineVersionWarn(
'x.y.z',
packageJSON.engines.node,
);
const nodeEngineWarnArray = cleanup(nodeEngineWarn);
nodeEngineWarn = nodeEngineWarnArray.join('\n');
msg = cleanup(msg).join('\n');
return msg === nodeEngineWarn || nodeEngineWarnArray.includes(msg);
}
/** /**
* Filter stdout / stderr lines to remove lines that start with `[debug]` and * Filter stdout / stderr lines to remove lines that start with `[debug]` and
* other lines that can be ignored for testing purposes. * other lines that can be ignored for testing purposes.
* @param testOutput * @param testOutput
*/ */
function filterCliOutputForTests(testOutput: TestOutput): TestOutput { export function filterCliOutputForTests({
const { matchesNodeEngineVersionWarn } = err,
require('../automation/utils') as typeof import('../automation/utils'); out,
}: {
err: string[];
out: string[];
}): { err: string[]; out: string[] } {
return { return {
exitCode: testOutput.exitCode, err: err.filter(
err: testOutput.err.filter(
(line: string) => (line: string) =>
line &&
!line.match(/\[debug\]/i) && !line.match(/\[debug\]/i) &&
// TODO stop this warning message from appearing when running // TODO stop this warning message from appearing when running
// sdk.setSharedOptions multiple times in the same process // sdk.setSharedOptions multiple times in the same process
@ -52,7 +75,7 @@ function filterCliOutputForTests(testOutput: TestOutput): TestOutput {
!line.startsWith('WARN: disabling Sentry.io error reporting') && !line.startsWith('WARN: disabling Sentry.io error reporting') &&
!matchesNodeEngineVersionWarn(line), !matchesNodeEngineVersionWarn(line),
), ),
out: testOutput.out.filter((line: string) => !line.match(/\[debug\]/i)), out: out.filter((line: string) => line && !line.match(/\[debug\]/i)),
}; };
} }
@ -61,6 +84,9 @@ function filterCliOutputForTests(testOutput: TestOutput): TestOutput {
* @param cmd Command to execute, e.g. `push myApp` (without 'balena' prefix) * @param cmd Command to execute, e.g. `push myApp` (without 'balena' prefix)
*/ */
async function runCommandInProcess(cmd: string): Promise<TestOutput> { async function runCommandInProcess(cmd: string): Promise<TestOutput> {
const balenaCLI = await import('../build/app');
const intercept = await import('intercept-stdout');
const preArgs = [process.argv[0], path.join(process.cwd(), 'bin', 'balena')]; const preArgs = [process.argv[0], path.join(process.cwd(), 'bin', 'balena')];
const err: string[] = []; const err: string[] = [];
@ -79,18 +105,19 @@ async function runCommandInProcess(cmd: string): Promise<TestOutput> {
const unhookIntercept = intercept(stdoutHook, stderrHook); const unhookIntercept = intercept(stdoutHook, stderrHook);
try { try {
await balenaCLI.run(preArgs.concat(cmd.split(' ')), { await balenaCLI.run(preArgs.concat(cmd.split(' ').filter((c) => c)), {
noFlush: true, noFlush: true,
}); });
} finally { } finally {
unhookIntercept(); unhookIntercept();
} }
return filterCliOutputForTests({ const filtered = filterCliOutputForTests({ err, out });
err, return {
out, err: filtered.err,
out: filtered.out,
// this makes sense if `process.exit()` was stubbed with sinon // this makes sense if `process.exit()` was stubbed with sinon
exitCode: process.exitCode, exitCode: process.exitCode,
}); };
} }
/** /**
@ -129,10 +156,11 @@ async function runCommandInSubprocess(
// override default proxy exclusion to allow proxying of requests to 127.0.0.1 // override default proxy exclusion to allow proxying of requests to 127.0.0.1
BALENARC_DO_PROXY: '127.0.0.1,localhost', BALENARC_DO_PROXY: '127.0.0.1,localhost',
}; };
const { execFile } = await import('child_process');
await new Promise<void>((resolve) => { await new Promise<void>((resolve) => {
const child = execFile( const child = execFile(
standalonePath, standalonePath,
cmd.split(' '), cmd.split(' ').filter((c) => c),
{ env: { ...process.env, ...addedEnvs } }, { env: { ...process.env, ...addedEnvs } },
($error, $stdout, $stderr) => { ($error, $stdout, $stderr) => {
stderr = $stderr || ''; stderr = $stderr || '';
@ -141,11 +169,12 @@ async function runCommandInSubprocess(
// non-zero exit code. Usually this is harmless/expected, as // non-zero exit code. Usually this is harmless/expected, as
// the CLI child process is tested for error conditions. // the CLI child process is tested for error conditions.
if ($error && process.env.DEBUG) { if ($error && process.env.DEBUG) {
console.error(` const msg = `
[debug] Error (possibly expected) executing child CLI process "${standalonePath}" Error (possibly expected) executing child CLI process "${standalonePath}"
------------------------------------------------------------------ ${$error}`;
${$error} const { warnify } =
------------------------------------------------------------------`); require('../build/utils/messages') as typeof import('../build/utils/messages');
console.error(warnify(msg, '[debug] '));
} }
resolve(); resolve();
}, },
@ -166,11 +195,16 @@ ${$error}
.filter((l) => l) .filter((l) => l)
.map((l) => l + '\n'); .map((l) => l + '\n');
return filterCliOutputForTests({ const filtered = filterCliOutputForTests({
exitCode,
err: splitLines(stderr), err: splitLines(stderr),
out: splitLines(stdout), out: splitLines(stdout),
}); });
return {
err: filtered.err,
out: filtered.out,
// this makes sense if `process.exit()` was stubbed with sinon
exitCode,
};
} }
/** /**
@ -190,11 +224,12 @@ export async function runCommand(cmd: string): Promise<TestOutput> {
); );
} }
try { try {
const { promises: fs } = await import('fs');
await fs.access(standalonePath); await fs.access(standalonePath);
} catch { } catch {
throw new Error(`Standalone executable not found: "${standalonePath}"`); throw new Error(`Standalone executable not found: "${standalonePath}"`);
} }
const proxy = await import('./proxy-server'); const proxy = await import('./nock/proxy-server');
const [proxyPort] = await proxy.createProxyServerOnce(); const [proxyPort] = await proxy.createProxyServerOnce();
return runCommandInSubprocess(cmd, proxyPort); return runCommandInSubprocess(cmd, proxyPort);
} else { } else {
@ -202,22 +237,6 @@ export async function runCommand(cmd: string): Promise<TestOutput> {
} }
} }
export const balenaAPIMock = () => {
if (!nock.isActive()) {
nock.activate();
}
return nock(/./).get('/config/vars').reply(200, {
reservedNames: [],
reservedNamespaces: [],
invalidRegex: '/^d|W/',
whiteListedNames: [],
whiteListedNamespaces: [],
blackListedNames: [],
configVarSchema: [],
});
};
export function cleanOutput( export function cleanOutput(
output: string[] | string, output: string[] | string,
collapseBlank = false, collapseBlank = false,
@ -226,11 +245,17 @@ export function cleanOutput(
? (line: string) => monochrome(line.trim()).replace(/\s{2,}/g, ' ') ? (line: string) => monochrome(line.trim()).replace(/\s{2,}/g, ' ')
: (line: string) => monochrome(line.trim()); : (line: string) => monochrome(line.trim());
return _(_.castArray(output)) const result: string[] = [];
.map((log: string) => log.split('\n').map(cleanLine)) output = typeof output === 'string' ? [output] : output;
.flatten() for (const lines of output) {
.compact() for (let line of lines.split('\n')) {
.value(); line = cleanLine(line);
if (line) {
result.push(line);
}
}
}
return result;
} }
/** /**
@ -320,6 +345,7 @@ export function deepJsonParse(data: any): any {
export async function switchSentry( export async function switchSentry(
enabled: boolean | undefined, enabled: boolean | undefined,
): Promise<boolean | undefined> { ): Promise<boolean | undefined> {
const balenaCLI = await import('../build/app');
const sentryOpts = (await balenaCLI.setupSentry()).getClient()?.getOptions(); const sentryOpts = (await balenaCLI.setupSentry()).getClient()?.getOptions();
if (sentryOpts) { if (sentryOpts) {
const sentryStatus = sentryOpts.enabled; const sentryStatus = sentryOpts.enabled;

View File

@ -21,7 +21,7 @@ import * as path from 'path';
import { NockMock, ScopeOpts } from './nock-mock'; import { NockMock, ScopeOpts } from './nock-mock';
export const apiResponsePath = path.normalize( export const apiResponsePath = path.normalize(
path.join(__dirname, 'test-data', 'api-response'), path.join(__dirname, '..', 'test-data', 'api-response'),
); );
const jHeader = { 'Content-Type': 'application/json' }; const jHeader = { 'Content-Type': 'application/json' };
@ -35,6 +35,7 @@ export class BalenaAPIMock extends NockMock {
notFound = false, notFound = false,
optional = false, optional = false,
persist = false, persist = false,
expandArchitecture = false,
} = {}) { } = {}) {
const interceptor = this.optGet(/^\/v6\/application($|[(?])/, { const interceptor = this.optGet(/^\/v6\/application($|[(?])/, {
optional, optional,
@ -45,7 +46,12 @@ export class BalenaAPIMock extends NockMock {
} else { } else {
interceptor.replyWithFile( interceptor.replyWithFile(
200, 200,
path.join(apiResponsePath, 'application-GET-v6-expanded-app-type.json'), path.join(
apiResponsePath,
!expandArchitecture
? 'application-GET-v6-expanded-app-type.json'
: 'application-GET-v6-expanded-app-type-cpu-arch.json',
),
jHeader, jHeader,
); );
} }
@ -72,10 +78,10 @@ export class BalenaAPIMock extends NockMock {
} }
public expectApplicationProvisioning(opts: ScopeOpts = {}) { public expectApplicationProvisioning(opts: ScopeOpts = {}) {
this.optPost(/^\/api-key\/application\/[0-9]+\/provisioning$/, opts).reply( // The endpoint changed in balena-sdk v15.45.0:
200, // before: '/api-key/application/${applicationId}/provisioning'
'dummykey', // after: '/api-key/v1/'
); this.optPost(/^\/api-key\/v[0-9]\/?$/, opts).reply(200, 'dummykey');
} }
public expectGetMyApplication(opts: ScopeOpts = {}) { public expectGetMyApplication(opts: ScopeOpts = {}) {
@ -95,12 +101,27 @@ export class BalenaAPIMock extends NockMock {
}); });
} }
public expectGetRelease(opts: ScopeOpts = {}) { public expectGetRelease({
this.optGet(/^\/v6\/release($|[(?])/, opts).replyWithFile( notFound = false,
200, optional = false,
path.join(apiResponsePath, 'release-GET-v6.json'), persist = false,
jHeader, } = {}) {
); const interceptor = this.optGet(/^\/v6\/release($|[(?])/, {
persist,
optional,
});
if (notFound) {
interceptor.reply(200, { d: [] });
} else {
this.optGet(/^\/v6\/release($|[(?])/, {
persist,
optional,
}).replyWithFile(
200,
path.join(apiResponsePath, 'release-GET-v6.json'),
jHeader,
);
}
} }
/** /**
@ -122,11 +143,18 @@ export class BalenaAPIMock extends NockMock {
/** /**
* Mocks balena-release call * Mocks balena-release call
*/ */
public expectPostRelease(opts: ScopeOpts = {}) { public expectPostRelease({
this.optPost(/^\/v6\/release($|[(?])/, opts).replyWithFile( statusCode = 200,
200, inspectRequest = this.inspectNoOp,
path.join(apiResponsePath, 'release-POST-v6.json'), optional = false,
jHeader, persist = false,
}) {
this.optPost(/^\/v6\/release($|[(?])/, { optional, persist }).reply(
statusCode,
this.getInspectedReplyFileFunction(
inspectRequest,
path.join(apiResponsePath, 'release-POST-v6.json'),
),
); );
} }
@ -198,7 +226,7 @@ export class BalenaAPIMock extends NockMock {
is_online: opts.isOnline, is_online: opts.isOnline,
belongs_to__application: opts.inaccessibleApp belongs_to__application: opts.inaccessibleApp
? [] ? []
: [{ app_name: 'test' }], : [{ app_name: 'test', slug: 'org/test' }],
}, },
], ],
}); });

View File

@ -22,7 +22,7 @@ import * as zlib from 'zlib';
import { NockMock } from './nock-mock'; import { NockMock } from './nock-mock';
export const builderResponsePath = path.normalize( export const builderResponsePath = path.normalize(
path.join(__dirname, 'test-data', 'builder-response'), path.join(__dirname, '..', 'test-data', 'builder-response'),
); );
export class BuilderMock extends NockMock { export class BuilderMock extends NockMock {

View File

@ -15,13 +15,13 @@
* limitations under the License. * limitations under the License.
*/ */
import * as _ from 'lodash';
import * as path from 'path'; import * as path from 'path';
import * as qs from 'querystring';
import { NockMock, ScopeOpts } from './nock-mock'; import { NockMock, ScopeOpts } from './nock-mock';
export const dockerResponsePath = path.normalize( export const dockerResponsePath = path.normalize(
path.join(__dirname, 'test-data', 'docker-response'), path.join(__dirname, '..', 'test-data', 'docker-response'),
); );
export class DockerMock extends NockMock { export class DockerMock extends NockMock {
@ -78,7 +78,7 @@ export class DockerMock extends NockMock {
checkBuildRequestBody: (requestBody: string) => Promise<void>; checkBuildRequestBody: (requestBody: string) => Promise<void>;
}) { }) {
this.optPost( this.optPost(
new RegExp(`^/build\\?(|.+&)t=${_.escapeRegExp(opts.tag)}&`), new RegExp(`^/build\\?(|.+&)${qs.stringify({ t: opts.tag })}&`),
opts, opts,
).reply(async function (uri, requestBody, cb) { ).reply(async function (uri, requestBody, cb) {
let error: Error | null = null; let error: Error | null = null;
@ -133,4 +133,42 @@ export class DockerMock extends NockMock {
}, },
); );
} }
public expectGetManifestBusybox(opts: ScopeOpts = {}) {
// this.optGet(/^\/distribution\/.*/, opts).replyWithFile(
this.optGet('/distribution/busybox/json', opts).replyWithFile(
200,
path.join(dockerResponsePath, 'distribution-busybox-GET.json'),
{
'api-version': '1.38',
'Content-Type': 'application/json',
},
);
}
public expectGetManifestRpi3Alpine(opts: ScopeOpts = {}) {
this.optGet(
'/distribution/balenalib/raspberrypi3-alpine/json',
opts,
).replyWithFile(
200,
path.join(dockerResponsePath, 'distribution-rpi3alpine.json'),
{
'api-version': '1.38',
'Content-Type': 'application/json',
},
);
}
public expectGetManifestNucAlpine(opts: ScopeOpts = {}) {
// NOTE: This URL does no work in real life... it's "intel-nuc", not "nuc"
this.optGet('/distribution/balenalib/nuc-alpine/json', opts).replyWithFile(
200,
path.join(dockerResponsePath, 'distribution-nucalpine.json'),
{
'api-version': '1.38',
'Content-Type': 'application/json',
},
);
}
} }

View File

@ -16,6 +16,7 @@
*/ */
import * as nock from 'nock'; import * as nock from 'nock';
import * as fs from 'fs';
export interface ScopeOpts { export interface ScopeOpts {
optional?: boolean; optional?: boolean;
@ -32,7 +33,10 @@ export class NockMock {
public readonly expect; public readonly expect;
protected static instanceCount = 0; protected static instanceCount = 0;
constructor(public basePathPattern: string | RegExp) { constructor(
public basePathPattern: string | RegExp,
public allowUnmocked: boolean = false,
) {
if (NockMock.instanceCount === 0) { if (NockMock.instanceCount === 0) {
if (!nock.isActive()) { if (!nock.isActive()) {
nock.activate(); nock.activate();
@ -44,7 +48,7 @@ export class NockMock {
); );
} }
NockMock.instanceCount += 1; NockMock.instanceCount += 1;
this.scope = nock(this.basePathPattern); this.scope = nock(this.basePathPattern, { allowUnmocked });
this.expect = this.scope; this.expect = this.scope;
} }
@ -103,6 +107,27 @@ export class NockMock {
}; };
} }
protected getInspectedReplyFileFunction(
inspectRequest: (uri: string, requestBody: nock.Body) => void,
replyBodyFile: string,
) {
return function (
this: nock.ReplyFnContext,
uri: string,
requestBody: nock.Body,
cb: (err: NodeJS.ErrnoException | null, result: nock.ReplyBody) => void,
) {
try {
inspectRequest(uri, requestBody);
} catch (err) {
cb(err, '');
}
const replyBody = fs.readFileSync(replyBodyFile);
cb(null, replyBody);
};
}
public done() { public done() {
try { try {
// scope.done() will throw an error if there are expected api calls that have not happened. // scope.done() will throw an error if there are expected api calls that have not happened.

50
tests/nock/npm-mock.ts Normal file
View File

@ -0,0 +1,50 @@
/**
* @license
* Copyright 2020 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.
*/
import { NockMock } from './nock-mock';
const jHeader = { 'Content-Type': 'application/json' };
export class NpmMock extends NockMock {
constructor() {
super(/registry\.npmjs\.org/);
}
public expectGetBalenaCli({
version,
publishedAt,
notFound = false,
optional = false,
persist = false,
}: {
version: string;
publishedAt: string;
notFound?: boolean;
optional?: boolean;
persist?: boolean;
}) {
const interceptor = this.optGet(`/balena-cli/${version}`, {
optional,
persist,
});
if (notFound) {
interceptor.reply(404, `version not found: ${version}`, jHeader);
} else {
interceptor.reply(200, { versionist: { publishedAt } }, jHeader);
}
}
}

View File

@ -22,7 +22,7 @@ import { Readable } from 'stream';
import { NockMock, ScopeOpts } from './nock-mock'; import { NockMock, ScopeOpts } from './nock-mock';
export const dockerResponsePath = path.normalize( export const dockerResponsePath = path.normalize(
path.join(__dirname, 'test-data', 'docker-response'), path.join(__dirname, '..', 'test-data', 'docker-response'),
); );
export class SupervisorMock extends NockMock { export class SupervisorMock extends NockMock {

View File

@ -86,7 +86,6 @@ export async function addRegSecretsEntries(
export function getDockerignoreWarn1(paths: string[], cmd: string) { export function getDockerignoreWarn1(paths: string[], cmd: string) {
const lines = [ const lines = [
'[Warn] ----------------------------------------------------------------------',
'[Warn] The following .dockerignore file(s) will not be used:', '[Warn] The following .dockerignore file(s) will not be used:',
]; ];
lines.push(...paths.map((p) => `[Warn] * ${p}`)); lines.push(...paths.map((p) => `[Warn] * ${p}`));
@ -96,7 +95,6 @@ export function getDockerignoreWarn1(paths: string[], cmd: string) {
'[Warn] root) is used. Microservices (multicontainer) fleets may use a separate', '[Warn] root) is used. Microservices (multicontainer) fleets may use a separate',
'[Warn] .dockerignore file for each service with the --multi-dockerignore (-m)', '[Warn] .dockerignore file for each service with the --multi-dockerignore (-m)',
`[Warn] option. See "balena help ${cmd}" for more details.`, `[Warn] option. See "balena help ${cmd}" for more details.`,
'[Warn] ----------------------------------------------------------------------',
], ],
); );
return lines; return lines;
@ -104,7 +102,6 @@ export function getDockerignoreWarn1(paths: string[], cmd: string) {
export function getDockerignoreWarn2(paths: string[], cmd: string) { export function getDockerignoreWarn2(paths: string[], cmd: string) {
const lines = [ const lines = [
'[Warn] ----------------------------------------------------------------------',
'[Warn] The following .dockerignore file(s) will not be used:', '[Warn] The following .dockerignore file(s) will not be used:',
]; ];
lines.push(...paths.map((p) => `[Warn] * ${p}`)); lines.push(...paths.map((p) => `[Warn] * ${p}`));
@ -114,7 +111,6 @@ export function getDockerignoreWarn2(paths: string[], cmd: string) {
"[Warn] root of each service's build context (in a microservices/multicontainer", "[Warn] root of each service's build context (in a microservices/multicontainer",
'[Warn] fleet), plus a .dockerignore file at the overall project root, are used.', '[Warn] fleet), plus a .dockerignore file at the overall project root, are used.',
`[Warn] See "balena help ${cmd}" for more details.`, `[Warn] See "balena help ${cmd}" for more details.`,
'[Warn] ----------------------------------------------------------------------',
], ],
); );
return lines; return lines;

View File

@ -0,0 +1,53 @@
{
"d": [
{
"application_type": [
{
"name": "Starter",
"slug": "microservices-starter",
"supports_multicontainer": true,
"is_legacy": false,
"__metadata": {}
}
],
"id": 1301645,
"user": {
"__deferred": {
"uri": "/resin/user(43699)"
},
"__id": 43699
},
"organization": [
{
"handle": "gh_user"
}
],
"depends_on__application": null,
"actor": 3423895,
"app_name": "testApp",
"slug": "gh_user/testApp",
"should_be__running_release": [
{
"commit": "96eec431d57e6976d3a756df33fde7e2"
}
],
"is_for__device_type": [
{
"slug": "raspberrypi3",
"is_of__cpu_architecture": [
{
"slug": "armv7hf"
}
]
}
],
"should_track_latest_release": true,
"is_accessible_by_support_until__date": null,
"is_public": false,
"is_host": false,
"__metadata": {
"uri": "/resin/application(@id)?@id=1301645"
}
}
]
}

View File

@ -1,52 +1,95 @@
{ {
"d": [ "d": [
{ {
"contains__image": [ "id": 142334,
{ "commit": "90247b54de4fa7a0a3cbc85e73c68039",
"image": [ "created_at": "2021-08-25T22:18:34.014Z",
{ "status": "success",
"id": 1820810, "semver": "0.0.0",
"created_at": "2020-01-04T01:13:08.805Z", "is_final": false,
"start_timestamp": "2020-01-04T01:13:08.583Z", "build_log": null,
"end_timestamp": "2020-01-04T01:13:11.920Z", "start_timestamp": "2021-08-25T22:18:33.624Z",
"dockerfile": "# FROM busybox\n# FROM arm32v7/busybox\n# FROM arm32v7/alpine\n# FROM eu.gcr.io/buoyant-idea-226013/arm32v7/busybox\n# FROM eu.gcr.io/buoyant-idea-226013/amd64/busybox\n# FROM balenalib/raspberrypi3-debian:jessie-build\nFROM balenalib/raspberrypi3:stretch\nENV UDEV=1\n\n# FROM sander85/rpi-busybox # armv6\n# FROM balenalib/raspberrypi3-alpine\n\n# COPY start.sh /\n# COPY /src/start.sh /src/start.sh\n# COPY /src/hello.txt /\n# COPY src/hi.txt /\n\n# RUN cat /hello.txt\n# RUN cat /hi.txt\n# RUN cat /run/secrets/my-secret.txt\n# EXPOSE 80\nRUN uname -a\n\n# FROM alpine\n# RUN apk update && apk add bash\n# SHELL [\"/bin/bash\", \"-c\"]\n# CMD for ((i=1; i > 0; i++)); do echo \"(Plain Dockerfile 34-$i) $(uname -a)\"; sleep ${INTERVAL=5}; done\n\n# CMD i=1; while :; do echo \"Plain Dockerfile 36 ($i) $(uname -a)\"; sleep 10; i=$((i+1)); done\n# ENTRYPOINT [\"/usr/bin/entry.sh\"]\nCMD [\"/bin/bash\"]\n", "end_timestamp": "2021-08-25T22:18:48.820Z",
"is_a_build_of__service": { "__metadata": {
"__deferred": { "uri": "/resin/release(@id)?@id=142334"
"uri": "/resin/service(233455)" },
}, "contains__image": [
"__id": 233455 {
}, "image": [
"image_size": 134320410, {
"is_stored_at__image_location": "registry2.balena-cloud.com/v2/9c00c9413942cd15cfc9189c5dac359d", "id": 1820810,
"project_type": "Standard Dockerfile", "created_at": "2020-01-04T01:13:08.805Z",
"error_message": null, "start_timestamp": "2020-01-04T01:13:08.583Z",
"build_log": "Step 1/4 : FROM balenalib/raspberrypi3:stretch\n ---> 8a75ea61d9c0\nStep 2/4 : ENV UDEV=1\n\u001b[42m\u001b[30mUsing cache\u001b[39m\u001b[49m\n ---> 159206067c8a\nStep 3/4 : RUN uname -a\n\u001b[42m\u001b[30mUsing cache\u001b[39m\u001b[49m\n ---> dd1b3d9c334b\nStep 4/4 : CMD [\"/bin/bash\"]\n\u001b[42m\u001b[30mUsing cache\u001b[39m\u001b[49m\n ---> 5211b6f4bb72\nSuccessfully built 5211b6f4bb72\n", "end_timestamp": "2020-01-04T01:13:11.920Z",
"push_timestamp": "2020-01-04T01:13:14.415Z", "dockerfile": "# FROM busybox\n# FROM arm32v7/busybox\n# FROM arm32v7/alpine\n# FROM eu.gcr.io/buoyant-idea-226013/arm32v7/busybox\n# FROM eu.gcr.io/buoyant-idea-226013/amd64/busybox\n# FROM balenalib/raspberrypi3-debian:jessie-build\nFROM balenalib/raspberrypi3:stretch\nENV UDEV=1\n\n# FROM sander85/rpi-busybox # armv6\n# FROM balenalib/raspberrypi3-alpine\n\n# COPY start.sh /\n# COPY /src/start.sh /src/start.sh\n# COPY /src/hello.txt /\n# COPY src/hi.txt /\n\n# RUN cat /hello.txt\n# RUN cat /hi.txt\n# RUN cat /run/secrets/my-secret.txt\n# EXPOSE 80\nRUN uname -a\n\n# FROM alpine\n# RUN apk update && apk add bash\n# SHELL [\"/bin/bash\", \"-c\"]\n# CMD for ((i=1; i > 0; i++)); do echo \"(Plain Dockerfile 34-$i) $(uname -a)\"; sleep ${INTERVAL=5}; done\n\n# CMD i=1; while :; do echo \"Plain Dockerfile 36 ($i) $(uname -a)\"; sleep 10; i=$((i+1)); done\n# ENTRYPOINT [\"/usr/bin/entry.sh\"]\nCMD [\"/bin/bash\"]\n",
"status": "success", "is_a_build_of__service": {
"content_hash": "sha256:6b5471aae43ae81e8f69e10d1a516cb412569a6d5020a57eae311f8fa16d688a", "__deferred": {
"contract": null, "uri": "/resin/service(233455)"
"__metadata": { },
"uri": "/resin/image(@id)?@id=1820810" "__id": 233455
} },
} "image_size": 134320410,
], "is_stored_at__image_location": "registry2.balena-cloud.com/v2/9c00c9413942cd15cfc9189c5dac359d",
"id": 1738663, "project_type": "Standard Dockerfile",
"created_at": "2020-01-04T01:13:14.646Z", "error_message": null,
"is_part_of__release": { "build_log": "Step 1/4 : FROM balenalib/raspberrypi3:stretch\n ---> 8a75ea61d9c0\nStep 2/4 : ENV UDEV=1\n\u001b[42m\u001b[30mUsing cache\u001b[39m\u001b[49m\n ---> 159206067c8a\nStep 3/4 : RUN uname -a\n\u001b[42m\u001b[30mUsing cache\u001b[39m\u001b[49m\n ---> dd1b3d9c334b\nStep 4/4 : CMD [\"/bin/bash\"]\n\u001b[42m\u001b[30mUsing cache\u001b[39m\u001b[49m\n ---> 5211b6f4bb72\nSuccessfully built 5211b6f4bb72\n",
"__deferred": { "push_timestamp": "2020-01-04T01:13:14.415Z",
"uri": "/resin/release(1203844)" "status": "success",
}, "content_hash": "sha256:6b5471aae43ae81e8f69e10d1a516cb412569a6d5020a57eae311f8fa16d688a",
"__id": 1203844 "contract": null,
}, "__metadata": {
"__metadata": { "uri": "/resin/image(@id)?@id=1820810"
"uri": "/resin/image__is_part_of__release(@id)?@id=1738663"
}
} }
}
], ],
"id": 1203844, "id": 1738663,
"created_at": "2020-01-04T01:13:14.646Z",
"is_part_of__release": {
"__deferred": {
"uri": "/resin/release(1203844)"
},
"__id": 1203844
},
"__metadata": { "__metadata": {
"uri": "/resin/release(@id)?@id=1203844" "uri": "/resin/image__is_part_of__release(@id)?@id=1738663"
} }
}
],
"release_tag": [
{
"tag_key": "testtag1",
"value": "val1",
"__metadata": {}
}
],
"composition": {
"version": "2.1",
"networks": {},
"volumes": {
"resin-data": {}
},
"services": {
"main": {
"build": {
"context": "."
},
"privileged": true,
"tty": true,
"restart": "always",
"network_mode": "host",
"volumes": [
"resin-data:/data"
],
"labels": {
"io.resin.features.kernel-modules": "1",
"io.resin.features.firmware": "1",
"io.resin.features.dbus": "1",
"io.resin.features.supervisor-api": "1",
"io.resin.features.resin-api": "1"
}
}
}
} }
}
] ]
} }

View File

@ -0,0 +1,53 @@
{
"Descriptor": {
"mediaType": "application/vnd.docker.distribution.manifest.list.v2+json",
"digest": "sha256:52f73a0a43a16cf37cd0720c90887ce972fe60ee06a687ee71fb93a7ca601df7",
"size": 2295
},
"Platforms": [
{
"architecture": "amd64",
"os": "linux"
},
{
"architecture": "arm",
"os": "linux",
"variant": "v5"
},
{
"architecture": "arm",
"os": "linux",
"variant": "v6"
},
{
"architecture": "arm",
"os": "linux",
"variant": "v7"
},
{
"architecture": "arm64",
"os": "linux",
"variant": "v8"
},
{
"architecture": "386",
"os": "linux"
},
{
"architecture": "mips64le",
"os": "linux"
},
{
"architecture": "ppc64le",
"os": "linux"
},
{
"architecture": "riscv64",
"os": "linux"
},
{
"architecture": "s390x",
"os": "linux"
}
]
}

View File

@ -0,0 +1,13 @@
{
"Descriptor": {
"mediaType": "application/vnd.docker.distribution.manifest.v2+json",
"digest": "sha256:d70bb0dd863198b41ea5d638993a9fbb912b3ea54b36480d1dc13e6b5b29021a",
"size": 2610
},
"Platforms": [
{
"architecture": "amd64",
"os": "linux"
}
]
}

View File

@ -0,0 +1,14 @@
{
"Descriptor": {
"mediaType": "application/vnd.docker.distribution.manifest.v2+json",
"digest": "sha256:2e33dc19d8514e01f7676532c507ddd95d0be20497fee25f4cbfc972cc6343d0",
"size": 2821
},
"Platforms": [
{
"architecture": "arm",
"os": "linux",
"variant": "v7"
}
]
}

View File

@ -1,5 +1,3 @@
> Warning Cannot find module 'net-keepalive' from 'build\utils\device'
%1: build\utils\device\api.js
> Warning Cannot resolve 'module' > Warning Cannot resolve 'module'
node_modules\balena-sync\build\index.js node_modules\balena-sync\build\index.js
Dynamic require may fail at run time, because the requested file Dynamic require may fail at run time, because the requested file

View File

@ -1 +1 @@
alternative Dockerfile (basic/service2) FROM busybox

View File

@ -0,0 +1,4 @@
FROM busybox
COPY ./src /usr/src/
RUN chmod a+x /usr/src/*.sh
CMD ["/usr/src/start.sh"]

View File

@ -0,0 +1,3 @@
name: testContract
type: sw.application
version: 1.5.2

View File

@ -0,0 +1,2 @@
#!/bin/sh
echo "Hello, test!"

View File

@ -7,6 +7,9 @@ import { FileIgnorer, IgnoreFileType } from '../../build/utils/ignore';
// of the FileIgnorer class to prevent a Typescript compilation error (this // of the FileIgnorer class to prevent a Typescript compilation error (this
// behavior is by design: see // behavior is by design: see
// https://github.com/microsoft/TypeScript/issues/19335 ) // https://github.com/microsoft/TypeScript/issues/19335 )
//
// v13: delete this file
//
describe('File ignorer', function () { describe('File ignorer', function () {
it('should detect ignore files', function () { it('should detect ignore files', function () {
const f = new FileIgnorer(`.${path.sep}`); const f = new FileIgnorer(`.${path.sep}`);