Compare commits

..

189 Commits

Author SHA1 Message Date
3f9288e9d3 v22.1.1 2025-06-19 09:32:56 +00:00
0efa745628 Merge pull request #2957 from balena-io/truncate-error-message
Deploy: Limit the submitted error_message of images that fail to build to 300K characters
2025-06-19 09:31:58 +00:00
bddad252f7 Deploy: Limit the submitted error_message of images that fail to build to 1000 characters
Change-type: patch
See: https://balena.fibery.io/Work/Project/re-pitching-API-Limit-size-of-large-fields-975
2025-06-19 11:28:38 +03:00
a1a0e4f028 Deduplicate dependencies 2025-06-17 23:47:57 +03:00
de74baa2ff v22.1.0 2025-06-09 20:05:12 +00:00
6c12f755c5 Merge pull request #2955 from balena-io/node-22
Node 22
2025-06-09 17:04:26 -03:00
f80b8e63b1 Add support for node 22
Change-type: minor
2025-06-07 11:14:48 -03:00
b32514f5af Bump etcher-sdk to v10.0.0
Update balena-device-init from 8.1.3 to 8.1.1
Update etcher-sdk from 9.1.4 to 10.0.0
Update resin-cli-form from 3.0.0 to 4.0.0
Update resin-cli-visuals from 2.0.1 to 3.0.0

Change-type: patch
2025-06-07 11:13:55 -03:00
935f8d2549 v22.0.6 2025-06-02 12:27:13 +00:00
b2de857ef1 Merge pull request #2952 from balena-io/remove-request
Remove request
2025-06-02 08:26:26 -04:00
78f1471bf4 Deduplicate dependencies 2025-05-30 08:23:23 -04:00
d47abf072d Remove request dependency
Change-type: patch
2025-05-30 08:16:08 -04:00
8502c4db4b Replace request usage with got
Change-type: patch
2025-05-30 08:16:08 -04:00
dd2c5c40d7 v22.0.5 2025-05-29 15:47:35 +00:00
d23b253ac5 Merge pull request #2951 from balena-io/bump-etcher-sdk
Bump etcher-sdk to v9.1.4
2025-05-29 12:46:43 -03:00
0b0e24c9b2 Bump etcher-sdk to v9.1.4
Update etcher-sdk from 9.1.0 to 9.1.4

Change-type: patch
2025-05-29 10:54:45 -03:00
f9656cbe91 v22.0.4 2025-05-29 13:19:04 +00:00
e174f7db4c Merge pull request #2949 from balena-io/use-got-instead-of-request-legacy-deploy
Use got instead of request legacy deploy
2025-05-29 13:18:10 +00:00
a7a408a5c7 tests: Replace request with got
Change-type: patch
2025-05-29 09:35:57 -03:00
e5877c7de9 deploy-legacy: Replace request with got
Change-type: patch
2025-05-29 09:35:57 -03:00
ecb8b3ae6b v22.0.3 2025-05-29 12:09:59 +00:00
fd20516f69 Merge pull request #2950 from balena-io/bump-sentry-v9
Bump sentry to v9
2025-05-29 12:09:11 +00:00
5ccee0e4f1 Bump sentry to v9
Change-type: patch
2025-05-28 20:29:40 -03:00
7bb13a551c v22.0.2 2025-05-28 19:32:18 +00:00
19d287aefc Merge pull request #2948 from balena-io/build-nologs
fix: allow balena build to work with --nologs
2025-05-28 16:31:28 -03:00
8d10c1af2a Fix balena build to work with --nologs
Change-type: patch
2025-05-28 15:36:56 -03:00
ebfabdba6a v22.0.1 2025-05-28 17:00:48 +00:00
b0cbe43708 Merge pull request #2947 from balena-io/nock-14
Update `nock` to 14.0.4 & Move DeviceAPI off `request`
2025-05-28 13:59:58 -03:00
b7f1469912 Deduplicate dependencies 2025-05-28 12:08:37 -04:00
3396ba5a97 DeviceAPI: Move away from request in favor of BalenaSdk request
Change-type: patch
2025-05-28 12:08:37 -04:00
78ffff83bc Update nock to 14.0.4
Change-type: patch
2025-05-28 08:35:40 -04:00
ae13d584a3 v22.0.0 2025-05-26 13:53:39 +00:00
14f12d17eb Merge pull request #2912 from balena-io/native-build-2
Build standalone without pkg
2025-05-26 10:52:55 -03:00
45eb0ad4b1 Add migration guide to v22
Change-type: patch
2025-05-24 08:17:07 -03:00
21fd8a3307 Build standalone without pkg
Change-type: major
2025-05-23 18:52:51 -03:00
b545bd00ad v21.1.14 2025-05-21 15:39:23 +00:00
3eda7938f9 Merge pull request #2946 from balena-io/bump-balena-preload-2
Bump balena-preload to 18.0.4
2025-05-21 12:38:05 -03:00
2bccabfc38 Bump balena-preload to 18.0.4
Update balena-preload from 18.0.3 to 18.0.4

Change-type: patch
2025-05-21 11:41:07 -03:00
1b42d08567 v21.1.13 2025-05-21 13:46:34 +00:00
55b69be987 Merge pull request #2944 from balena-io/bump-balena-preload
Bump balena-preload to 18.0.3
2025-05-21 13:45:38 +00:00
7c8ce1b1a9 Bump balena-preload to 18.0.3
Update balena-preload from 18.0.1 to 18.0.3

Change-type: patch
2025-05-21 09:58:51 -03:00
31ddacec4c v21.1.12 2025-05-16 18:54:51 +00:00
dad26328b9 Merge pull request #2935 from balena-io/fix-build-secrets-serialization
Fix build secrets serialization
2025-05-16 14:54:04 -04:00
98197eef47 Dedupe dependencies 2025-05-15 19:13:36 -04:00
0f8ce47ec6 Update @balena/compose dependency to fix error with build secrets.
See https://github.com/balena-io-modules/balena-compose/pull/79

Change-type: patch
Signed-off-by: Ken Bannister <kb2ma@runbox.com>
2025-05-15 19:03:04 -04:00
33d3be326a v21.1.11 2025-05-06 15:25:00 +00:00
3eb397de1b Merge pull request #2611 from balena-io/lmb/fix-typos
Fix typos in the help messages
2025-05-06 15:24:01 +00:00
457b81a597 Deduplicate dependencies 2025-05-06 10:57:39 -04:00
8eb1777437 Fix typos in os configure and config generate help messages
Change-type: patch
2025-05-06 09:08:01 -04:00
bbc08dcfc5 v21.1.10 2025-05-05 17:13:26 +00:00
48cee061f4 Merge pull request #2937 from balena-io/fix-windows-signing
patch: fix windows signing
2025-05-05 17:12:40 +00:00
37e96e5d67 patch: fix windows signing 2025-05-05 13:38:05 -03:00
a5c865b7f9 v21.1.9 2025-04-07 12:53:22 +00:00
3fb3dd5819 Merge pull request #2929 from balena-io/update-balena-config-json
Update balena-config-json to rely on the balena-image-fs helpers
2025-04-07 15:52:21 +03:00
daf5c518fb Deduplicate dependencies 2025-04-04 10:38:24 +03:00
4fcedd0607 Update balena-config-json to rely on the balena-image-fs helpers
Update balena-config-json from 4.2.2 to 4.2.7
Update balena-image-fs from 7.5.0 to 7.5.2

Change-type: patch
2025-04-04 10:23:39 +03:00
42d9cbb48d v21.1.8 2025-04-03 16:10:05 +00:00
408efa91c1 Merge pull request #2933 from balena-io/renovate/actions-download-artifact-4.1.x
Update actions/download-artifact action to v4.1.9
2025-04-03 16:09:08 +00:00
a2209ffe56 Update actions/download-artifact action to v4.1.9
Update actions/download-artifact from 4.1.8 to 4.1.9

Change-type: patch
2025-04-03 15:50:05 +00:00
3f27db811b v21.1.7 2025-04-03 15:12:16 +00:00
839a3050fb Merge pull request #2932 from balena-io/renovate/actions-upload-artifact-digest
Update actions/upload-artifact digest to ea165f8
2025-04-03 15:11:18 +00:00
c8ea9cfcdb Update actions/upload-artifact digest to ea165f8
Update actions/upload-artifact

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

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

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

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

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

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

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

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

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

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

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

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

Change-type: patch
2024-11-05 09:50:56 +00:00
04f5e0fa2b v20.0.4 2024-11-05 09:13:22 +00:00
8cb5804848 Merge pull request #2883 from balena-io/renovate/actions-setup-node-digest
Update actions/setup-node digest to 39370e3
2024-11-05 09:12:31 +00:00
91c3fced49 Update actions/setup-node digest to 39370e3
Update actions/setup-node

Change-type: patch
2024-11-05 08:52:19 +00:00
99a94eafbb v20.0.3 2024-11-05 08:12:01 +00:00
b4cff78588 Merge pull request #2885 from balena-io/non-sudo-api-keys
api-key generate: Display a descriptive error when the generation fails due to a stale JWT
2024-11-05 08:11:04 +00:00
8577bb6281 Deduplicate dependencies 2024-11-04 20:51:28 +02:00
e0f081623b api-key generate: Display a descriptive error when the generation fails due to a stale JWT
Change-type: patch
2024-11-04 20:51:27 +02:00
2887ab8200 v20.0.2 2024-10-29 12:57:11 +00:00
aaf4625abb Merge pull request #2881 from balena-io/fix-ssh-key-add-piping
Restore ability to cat key into `ssh-key add`
2024-10-29 12:56:13 +00:00
6f30dc0550 Restore ability to cat key into ssh-key add
Change-type: patch
2024-10-29 07:48:42 -04:00
126 changed files with 8617 additions and 7108 deletions

View File

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

View File

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

View File

@ -67,7 +67,7 @@ fixed it.
- **Cloud backend: openBalena or balenaCloud?** If unsure, it will be balenaCloud - **Cloud backend: openBalena or balenaCloud?** If unsure, it will be balenaCloud
- **Operating system version:** e.g. Windows 10, Ubuntu 18.04, macOS 10.14.5 - **Operating system version:** e.g. Windows 10, Ubuntu 18.04, macOS 10.14.5
- **32/64 bit OS and processor:** e.g. 32-bit Windows on 64-bit Intel processor - **32/64 bit OS and processor:** e.g. 32-bit Windows on 64-bit Intel processor
- **Install method:** npm or zip package or executable installer - **Install method:** npm or standalone package or executable installer
- **If npm install, Node.js and npm version:** e.g. Node v8.16.0 and npm v6.4.1 - **If npm install, Node.js and npm version:** e.g. Node v8.16.0 and npm v6.4.1
# Additional References # Additional References

View File

@ -18,7 +18,7 @@ inputs:
default: 'accounts+apple@balena.io' default: 'accounts+apple@balena.io'
NODE_VERSION: NODE_VERSION:
type: string type: string
default: '20.x' default: '22.x'
VERBOSE: VERBOSE:
type: string type: string
default: 'true' default: 'true'
@ -28,7 +28,7 @@ runs:
using: 'composite' using: 'composite'
steps: steps:
- name: Download custom source artifact - name: Download custom source artifact
uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8 uses: actions/download-artifact@cc203385981b70ca67e1cc392babf9cc229d5806 # v4.1.9
with: with:
name: custom-${{ github.event.pull_request.head.sha || github.event.head_commit.id }}-${{ runner.os }}-${{ runner.arch }} name: custom-${{ github.event.pull_request.head.sha || github.event.head_commit.id }}-${{ runner.os }}-${{ runner.arch }}
path: ${{ runner.temp }} path: ${{ runner.temp }}
@ -39,7 +39,7 @@ runs:
run: tar -xf ${{ runner.temp }}/custom.tgz run: tar -xf ${{ runner.temp }}/custom.tgz
- name: Setup Node.js - name: Setup Node.js
uses: actions/setup-node@1e60f620b9541d16bece96c5465dc8ee9832be0b # v4 uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4
with: with:
node-version: ${{ inputs.NODE_VERSION }} node-version: ${{ inputs.NODE_VERSION }}
cache: npm cache: npm
@ -48,7 +48,7 @@ runs:
if: runner.os == 'macOS' if: runner.os == 'macOS'
uses: actions/setup-python@65d7f2d534ac1bc67fcd62888c5f4f3d2cb2b236 # v4 uses: actions/setup-python@65d7f2d534ac1bc67fcd62888c5f4f3d2cb2b236 # v4
with: with:
python-version: "3.11" python-version: '3.11'
- name: Install additional tools - name: Install additional tools
if: runner.os == 'Windows' if: runner.os == 'Windows'
@ -94,7 +94,7 @@ runs:
runner_arch="$(echo "${RUNNER_ARCH}" | tr '[:upper:]' '[:lower:]')" runner_arch="$(echo "${RUNNER_ARCH}" | tr '[:upper:]' '[:lower:]')"
if [[ $runner_os =~ darwin|macos|osx ]]; then if [[ $runner_os =~ darwin|macos|osx ]]; then
CSC_KEY_PASSWORD=${{ fromJSON(inputs.secrets).APPLE_SIGNING_PASSWORD }} CSC_KEY_PASSWORD='${{ fromJSON(inputs.secrets).APPLE_SIGNING_PASSWORD }}'
CSC_KEYCHAIN=signing_temp CSC_KEYCHAIN=signing_temp
CSC_LINK=${{ fromJSON(inputs.secrets).APPLE_SIGNING }} CSC_LINK=${{ fromJSON(inputs.secrets).APPLE_SIGNING }}
@ -112,8 +112,8 @@ runs:
PATH="/c/Program Files/DigiCert/DigiCert One Signing Manager Tools:${PATH}" PATH="/c/Program Files/DigiCert/DigiCert One Signing Manager Tools:${PATH}"
smksp_registrar.exe list smksp_registrar.exe list
smctl.exe keypair ls smctl.exe keypair ls
smctl.exe windows certsync
/c/Windows/System32/certutil.exe -csp "DigiCert Signing Manager KSP" -key -user /c/Windows/System32/certutil.exe -csp "DigiCert Signing Manager KSP" -key -user
smksp_cert_sync.exe
# (signtool.exe) https://github.com/actions/runner-images/blob/main/images/win/Windows2019-Readme.md#installed-windows-sdks # (signtool.exe) https://github.com/actions/runner-images/blob/main/images/win/Windows2019-Readme.md#installed-windows-sdks
PATH="/c/Program Files (x86)/Windows Kits/10/bin/${runner_arch}:${PATH}" PATH="/c/Program Files (x86)/Windows Kits/10/bin/${runner_arch}:${PATH}"
@ -135,9 +135,11 @@ runs:
XCODE_APP_LOADER_TEAM_ID: ${{ inputs.XCODE_APP_LOADER_TEAM_ID }} XCODE_APP_LOADER_TEAM_ID: ${{ inputs.XCODE_APP_LOADER_TEAM_ID }}
- name: Upload artifacts - name: Upload artifacts
uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4 uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with: with:
name: gh-release-${{ github.event.pull_request.head.sha || github.event.head_commit.id }}-${{ strategy.job-index }} name: gh-release-${{ github.event.pull_request.head.sha || github.event.head_commit.id }}-${{ strategy.job-index }}
path: dist path: |
dist
!dist/balena
retention-days: 1 retention-days: 1
if-no-files-found: error if-no-files-found: error

View File

@ -15,7 +15,7 @@ inputs:
# --- custom environment # --- custom environment
NODE_VERSION: NODE_VERSION:
type: string type: string
default: '20.x' default: '22.x'
VERBOSE: VERBOSE:
type: string type: string
default: "true" default: "true"
@ -26,7 +26,7 @@ runs:
steps: steps:
# https://github.com/actions/setup-node#caching-global-packages-data # https://github.com/actions/setup-node#caching-global-packages-data
- name: Setup Node.js - name: Setup Node.js
uses: actions/setup-node@1e60f620b9541d16bece96c5465dc8ee9832be0b # v4 uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4
with: with:
node-version: ${{ inputs.NODE_VERSION }} node-version: ${{ inputs.NODE_VERSION }}
cache: npm cache: npm
@ -58,7 +58,7 @@ runs:
run: tar --exclude-vcs -acf ${{ runner.temp }}/custom.tgz . run: tar --exclude-vcs -acf ${{ runner.temp }}/custom.tgz .
- name: Upload custom artifact - name: Upload custom artifact
uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4 uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with: with:
name: custom-${{ github.event.pull_request.head.sha || github.event.head_commit.id }}-${{ runner.os }}-${{ runner.arch }} name: custom-${{ github.event.pull_request.head.sha || github.event.head_commit.id }}-${{ runner.os }}-${{ runner.arch }}
path: ${{ runner.temp }}/custom.tgz path: ${{ runner.temp }}/custom.tgz

View File

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

View File

@ -2,7 +2,7 @@ module.exports = {
reporter: 'spec', reporter: 'spec',
require: 'ts-node/register/transpile-only', require: 'ts-node/register/transpile-only',
file: './tests/config-tests', file: './tests/config-tests',
timeout: 12000, timeout: 48000,
// To test only, say, 'push.spec.ts', do it as follows so that // To test only, say, 'push.spec.ts', do it as follows so that
// requests are authenticated: // requests are authenticated:
// spec: ['tests/auth/*.spec.ts', 'tests/**/deploy.spec.ts'], // spec: ['tests/auth/*.spec.ts', 'tests/**/deploy.spec.ts'],

File diff suppressed because it is too large Load Diff

View File

@ -4,6 +4,466 @@ 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/).
## 22.1.1 - 2025-06-19
* Deploy: Limit the submitted error_message of images that fail to build to 1000 characters [Thodoris Greasidis]
## 22.1.0 - 2025-06-09
* Add support for node 22 [Otavio Jacobi]
<details>
<summary> Bump etcher-sdk to v10.0.0 [Otavio Jacobi] </summary>
> ### etcher-sdk-10.0.0 - 2025-06-02
>
> * Drop support to node18 and add support to node 22 & 24 [Otavio Jacobi]
>
</details>
## 22.0.6 - 2025-06-02
* Remove `request` dependency [myarmolinsky]
* Replace `request` usage with `got` [myarmolinsky]
## 22.0.5 - 2025-05-29
<details>
<summary> Bump etcher-sdk to v9.1.4 [Otavio Jacobi] </summary>
> ### etcher-sdk-9.1.4 - 2025-05-29
>
> * Run `npm audit fix` which should only do non-breaking changes [Otavio Jacobi]
>
> ### etcher-sdk-9.1.3 - 2025-02-17
>
> * Embed config.json with a fixed timestamp to enable consistent checksums [Pagan Gazzard]
>
> ### etcher-sdk-9.1.2 - 2024-10-09
>
> * Update dependency unzip-stream to v0.3.2 [SECURITY] [Self-hosted Renovate Bot]
>
> ### etcher-sdk-9.1.1 - 2024-10-09
>
> * patch: add EXLOCK flag for windows [Talha Can Havadar]
>
</details>
## 22.0.4 - 2025-05-29
* tests: Replace request with got [Otavio Jacobi]
* deploy-legacy: Replace request with got [Otavio Jacobi]
## 22.0.3 - 2025-05-29
* Bump sentry to v9 [Otavio Jacobi]
## 22.0.2 - 2025-05-28
* Fix balena build to work with --nologs [Otavio Jacobi]
## 22.0.1 - 2025-05-28
* DeviceAPI: Move away from `request` in favor of BalenaSdk request [myarmolinsky]
* Update `nock` to 14.0.4 [myarmolinsky]
## 22.0.0 - 2025-05-26
* Add migration guide to v22 [Otavio Jacobi]
* Build standalone without pkg [Otavio Jacobi]
## 21.1.14 - 2025-05-21
<details>
<summary> Bump balena-preload to 18.0.4 [Otavio Jacobi] </summary>
> ### balena-preload-18.0.4 - 2025-05-21
>
> * Fix balena-preload pip deps [Otavio Jacobi]
>
</details>
## 21.1.13 - 2025-05-21
<details>
<summary> Bump balena-preload to 18.0.3 [Otavio Jacobi] </summary>
> ### balena-preload-18.0.3 - 2025-03-19
>
> * Update dependency sh to v1.14.3 [balena-renovate[bot]]
>
> ### balena-preload-18.0.2 - 2025-03-19
>
> * Update alpine Docker tag to v3.21 [balena-renovate[bot]]
>
</details>
## 21.1.12 - 2025-05-16
* Update @balena/compose dependency to fix error with build secrets. [Ken Bannister]
## 21.1.11 - 2025-05-06
* Fix typos in `os configure` and `config generate` help messages [myarmolinsky]
## 21.1.10 - 2025-05-05
* patch: fix windows signing [Edwin Joassart]
## 21.1.9 - 2025-04-07
<details>
<summary> Update balena-config-json to rely on the balena-image-fs helpers [Thodoris Greasidis] </summary>
> ### balena-config-json-4.2.7 - 2025-04-02
>
> * Fix getBootPartition always warning that the boot partitions were not found [Thodoris Greasidis]
>
> ### balena-config-json-4.2.6 - 2025-04-01
>
> * write: Allow undefined as a value for the deprecated type parameter [Thodoris Greasidis]
>
> <details>
> <summary> Use the balena-image-fs findPartition() helper to find the boot partition [Thodoris Greasidis] </summary>
>
>> #### balena-image-fs-7.5.0 - 2025-03-26
>>
>> * Add function to find a partition by name/label [Ken Bannister]
>>
>> #### balena-image-fs-7.4.1 - 2025-02-21
>>
>> * bump ext2fs to 4.2.4 [Ryan Cooke]
>>
>
> </details>
>
>
> ### balena-config-json-4.2.5 - 2025-03-26
>
> * Update @balena/lint to 9.1.4 [Thodoris Greasidis]
>
> ### balena-config-json-4.2.4 - 2025-03-26
>
> * Switch use ts-mocha instead of manually compiling the tests [Thodoris Greasidis]
> * Update TypeScript to 5.8.2 [Thodoris Greasidis]
>
> ### balena-config-json-4.2.3 - 2025-03-26
>
> * Update chai to v5 [balena-renovate[bot]]
>
> ### balena-image-fs-7.5.2 - 2025-04-02
>
> * Update dependency jsdoc-to-markdown to v9 [balena-renovate[bot]]
>
> ### balena-image-fs-7.5.1 - 2025-03-26
>
> * Update @balena/lint to v9 [Thodoris Greasidis]
> * Remove the no longer needed gpt detection helper [Thodoris Greasidis]
> * Update TypeScript to 5.8.2 [Thodoris Greasidis]
> * Drop the package-lock.json [Thodoris Greasidis]
>
</details>
## 21.1.8 - 2025-04-03
* Update actions/download-artifact action to v4.1.9 [balena-renovate[bot]]
## 21.1.7 - 2025-04-03
* Update actions/upload-artifact digest to ea165f8 [balena-renovate[bot]]
## 21.1.6 - 2025-04-03
* Update actions/setup-node digest to cdca736 [balena-renovate[bot]]
## 21.1.5 - 2025-04-03
* Update dockerode/docker-modem dependencies for fixes [Ken Bannister]
## 21.1.4 - 2025-04-02
* Add comment with secure boot signature file example for preload [Ken Bannister]
## 21.1.3 - 2025-03-28
* Fix device detail for open balena [Otavio Jacobi]
## 21.1.2 - 2025-03-27
* Deny preload for an image with secure boot enabled [Ken Bannister]
## 21.1.1 - 2025-03-26
<details>
<summary> Bump balena-sdk to 21.3.0 [Otavio Jacobi] </summary>
> ### balena-sdk-21.3.0 - 2025-03-26
>
> * device: add `changed_api_heartbeat_state_on__date` to typings [Otavio Jacobi]
>
> ### balena-sdk-21.2.2 - 2025-03-26
>
> * fix linting [Otavio Jacobi]
>
</details>
## 21.1.0 - 2025-03-12
* Add support for new requirement labels feature [Felipe Lalanne]
## 21.0.0 - 2025-03-11
* Drop support for OS versions <2.14.0 [myarmolinsky]
* api-key generate: Add required argument `expiryDate` [myarmolinsky]
* Update `balena-preload` to 18.0.1 [myarmolinsky]
* Add dependency `date-fns` [myarmolinsky]
* Update `balena-sdk` to 21.2.1 [myarmolinsky]
## 20.2.10 - 2025-03-10
* Update TypeScript to 5.8.2 [Thodoris Greasidis]
## 20.2.9 - 2025-02-26
* Fix CORS issue with X-Balena-Client header [Thodoris Greasidis]
## 20.2.8 - 2025-02-26
* Update balena-config-json dependency and fix test [Ken Bannister]
## 20.2.7 - 2025-02-25
* Use the CLI version in the X-Balena-Client header [Thodoris Greasidis]
## 20.2.6 - 2025-02-25
* Update actions/upload-artifact digest to 4cec3d8 [balena-renovate[bot]]
## 20.2.5 - 2025-02-25
* Update actions/setup-node digest to 1d0ff46 [balena-renovate[bot]]
## 20.2.4 - 2025-02-25
* Pin docker-modem and dockerode to avoid regression [Ken Bannister]
## 20.2.3 - 2025-01-15
* Remove unused old eslint version files [Otavio Jacobi]
## 20.2.2 - 2025-01-12
* Use the promises namespace of balena-image-fs [Thodoris Greasidis]
<details>
<summary> Update balena-device-init to 8.1.3 & balena-image-fs to 7.3.0 [Thodoris Greasidis] </summary>
> ### balena-image-fs-7.3.0 - 2025-01-06
>
> * Drop Bluebird from devDependencies [Thodoris Greasidis]
> * flowzone: Remove empty with clause [Thodoris Greasidis]
> * Add the promises namespace as part of the exposed fs [Thodoris Greasidis]
>
> ### balena-image-fs-7.2.2 - 2024-01-02
>
> * Update dependency @types/node to v20 [Self-hosted Renovate Bot]
>
> ### balena-image-fs-7.2.1 - 2023-12-19
>
> * Remove repo config from flowzone.yml [Kyle Harding]
>
> ### balena-image-fs-7.2.0 - 2023-01-20
>
> * Add support for Node 18 [Akis Kesoglou]
>
> ### balena-image-fs-7.1.2 - 2023-01-05
>
> * Update dependencies [ab77]
>
> ### balena-image-fs-7.1.1 - 2022-12-20
>
> * Update dependency jsdoc-to-markdown to 8.0.0 [Renovate Bot]
>
> ### balena-image-fs-7.1.0 - 2022-12-13
>
> * update dependencies [Zane Hitchcox]
>
> ### balena-device-init-8.1.3 - 2025-01-09
>
> * README: Remove the travisci badge [Thodoris Greasidis]
>
> ### balena-device-init-8.1.2 - 2025-01-09
>
>
> <details>
> <summary> Use the promises namespace of balena-image-fs [Thodoris Greasidis] </summary>
>
>> #### balena-image-fs-7.3.0 - 2025-01-06
>>
>> * Drop Bluebird from devDependencies [Thodoris Greasidis]
>> * flowzone: Remove empty with clause [Thodoris Greasidis]
>> * Add the promises namespace as part of the exposed fs [Thodoris Greasidis]
>>
>> #### balena-image-fs-7.2.2 - 2024-01-02
>>
>> * Update dependency @types/node to v20 [Self-hosted Renovate Bot]
>>
>> #### balena-image-fs-7.2.1 - 2023-12-19
>>
>> * Remove repo config from flowzone.yml [Kyle Harding]
>>
>> #### balena-image-fs-7.2.0 - 2023-01-20
>>
>> * Add support for Node 18 [Akis Kesoglou]
>>
>> #### balena-image-fs-7.1.2 - 2023-01-05
>>
>> * Update dependencies [ab77]
>>
>> #### balena-image-fs-7.1.1 - 2022-12-20
>>
>> * Update dependency jsdoc-to-markdown to 8.0.0 [Renovate Bot]
>>
>> #### balena-image-fs-7.1.0 - 2022-12-13
>>
>> * update dependencies [Zane Hitchcox]
>>
>
> </details>
>
>
> ### balena-device-init-8.1.1 - 2025-01-06
>
> * Convert some parts to async await and simplify [Thodoris Greasidis]
> * Avoid unnecessary destructuring [Thodoris Greasidis]
>
</details>
## 20.2.1 - 2025-01-01
<details>
<summary> Update balena-preload to 17.0.0 [Thodoris Greasidis] </summary>
> ### balena-preload-17.0.0 - 2024-10-21
>
> * Improve typings [Thodoris Greasidis]
> * Stop returning Bluebird promises & drop it from the dependencies [Thodoris Greasidis]
>
</details>
## 20.2.0 - 2024-12-31
<details>
<summary> os configure: Give precedence to the boot partition located in the image over the device-type.json contents [Thodoris Greasidis] </summary>
> ### balena-device-init-8.1.0 - Invalid date
>
> * Try to find the boot partition by inspecting the image [Thodoris Greasidis]
>
> ### balena-device-init-8.0.1 - 2024-12-19
>
> * Drop the unnecessary eslint.config.js [Thodoris Greasidis]
> * packacke.json: Explicitly set type commonjs [Thodoris Greasidis]
>
</details>
## 20.1.6 - 2024-12-30
* Add more realistic os configure tests [Thodoris Greasidis]
## 20.1.5 - 2024-12-20
* Update shrinkwrapped express to v4.21.2 [Oskar Williams]
## 20.1.4 - 2024-12-20
<details>
<summary> Update balena-device-init to 8.0.0 [Thodoris Greasidis] </summary>
> ### balena-device-init-8.0.0 - 2024-12-18
>
> * Avoid running linting in the custom tests [Thodoris Greasidis]
> * Stop returning Bluebird promises [Thodoris Greasidis]
> * package: Publish only the build & typings folders [Thodoris Greasidis]
> * Convert to TypeScript with es6 module notation [Thodoris Greasidis]
> * Replace gulp, coffeelint & mochainon with tsc, @balena/lint, mocha, chai & sinon [Thodoris Greasidis]
> * Drop support for node <20.6.0 [Thodoris Greasidis]
>
> ### balena-device-init-7.0.2 - 2024-12-17
>
> * flowzone: Update runner versions [Thodoris Greasidis]
> * Pin etcher-sdk to 9.0.8 to match resin-device-operations and fix tests [Thodoris Greasidis]
>
</details>
## 20.1.3 - 2024-12-20
* Update oclif to 4.17.0 and @oclif/core 4.1.0 [Otavio Jacobi]
## 20.1.2 - 2024-12-17
* Remove unnecessary `Promise.resolve` and `Promise.reject` [Pagan Gazzard]
## 20.1.1 - 2024-12-16
* Update @balena/lint to v9.1.3 [Otavio Jacobi]
## 20.1.0 - 2024-12-12
* `device os-update`: Add handling for updates that require takeover [myarmolinsky]
* Update `balena-sdk` [myarmolinsky]
* Update `@balena/compose` [myarmolinsky]
## 20.0.9 - 2024-12-05
* Update shrinkwrapped express to v4.21.1 [Oskar Williams]
## 20.0.8 - 2024-12-04
* Run test and publish with macos-13 [Otavio Jacobi]
## 20.0.7 - 2024-11-23
* Update TypeScript to 5.7.2 [Thodoris Greasidis]
## 20.0.6 - 2024-11-08
* Refactor balena build for clarity [Thodoris Greasidis]
## 20.0.5 - 2024-11-05
* Update actions/upload-artifact digest to b4b15b8 [balena-renovate[bot]]
## 20.0.4 - 2024-11-05
* Update actions/setup-node digest to 39370e3 [balena-renovate[bot]]
## 20.0.3 - 2024-11-05
* api-key generate: Display a descriptive error when the generation fails due to a stale JWT [Thodoris Greasidis]
## 20.0.2 - 2024-10-29
* Restore ability to cat key into `ssh-key add` [myarmolinsky]
## 20.0.1 - 2024-10-29 ## 20.0.1 - 2024-10-29
* Fix sending input to some aliases not working [myarmolinsky] * Fix sending input to some aliases not working [myarmolinsky]

View File

@ -14,7 +14,7 @@ The balena CLI is an open source project and your contribution is welcome!
In order to ease development: In order to ease development:
* `npm run build:fast` skips some of the build steps for interactive testing, or * `npm run build:fast` skips some of the build steps for interactive testing, or
* `npm run test:source` skips testing the standalone zip packages (which is rather slow) * `npm run test:source` skips testing the standalone packages (which is rather slow)
* `./bin/balena-dev` uses `ts-node/register` to transpile on the fly. * `./bin/balena-dev` uses `ts-node/register` to transpile on the fly.
Before opening a PR, test your changes with `npm test`. Keep compatibility in mind, as the CLI is Before opening a PR, test your changes with `npm test`. Keep compatibility in mind, as the CLI is

View File

@ -8,8 +8,8 @@ There are 3 options to choose from to install balena's CLI:
* [Executable Installer](#executable-installer): the easiest method on Windows and macOS, using the * [Executable Installer](#executable-installer): the easiest method on Windows and macOS, using the
traditional graphical desktop application installers. traditional graphical desktop application installers.
* [Standalone Zip Package](#standalone-zip-package): these are plain zip files with the balena CLI * [Standalone tar.gz Package](#standalone-targz-package): these are plain tar.gz files with the balena CLI
executable in them: extract and run. Available for all platforms: Linux, Windows, macOS. bundled within. Available for all platforms: Linux, Windows, macOS.
Recommended also for scripted installation in CI (continuous integration) environments. Recommended also for scripted installation in CI (continuous integration) environments.
* [NPM Installation](#npm-installation): recommended for Node.js developers who may be interested * [NPM Installation](#npm-installation): recommended for Node.js developers who may be interested
in integrating the balena CLI in their existing projects or workflow. in integrating the balena CLI in their existing projects or workflow.
@ -30,9 +30,9 @@ instructions:
> If you would like to use WSL, follow the [installations instructions for > If you would like to use WSL, follow the [installations instructions for
> Linux](./INSTALL-LINUX.md) rather than Windows, as WSL consists of a Linux environment. > Linux](./INSTALL-LINUX.md) rather than Windows, as WSL consists of a Linux environment.
If you had previously installed the CLI using a standalone zip package, it may be a good idea to If you had previously installed the CLI using a standalone tar package, it may be a good idea to
check your system's `PATH` environment variable for duplicate entries, as the terminal will use the check your system's `PATH` environment variable for duplicate entries, as the terminal will use the
entry that comes first. Check the [Standalone Zip Package](#standalone-zip-package) instructions entry that comes first. Check the [Standalone tar.gz Package](#standalone-targz-package) instructions
for how to modify the PATH variable. for how to modify the PATH variable.
By default, the CLI is installed to the following folders: By default, the CLI is installed to the following folders:
@ -42,18 +42,17 @@ OS | Folders
Windows: | `C:\Program Files\balena-cli\` Windows: | `C:\Program Files\balena-cli\`
macOS: | `/usr/local/src/balena-cli/` <br> `/usr/local/bin/balena` macOS: | `/usr/local/src/balena-cli/` <br> `/usr/local/bin/balena`
## Standalone Zip Package ## Standalone tar.gz Package
1. Download the latest zip file from the [releases page](https://github.com/balena-io/balena-cli/releases). 1. Download the latest tar.gz file from the [releases page](https://github.com/balena-io/balena-cli/releases).
Look for a file name that ends with the word "standalone", for example: Look for a file name that ends with the word "standalone", for example:
`balena-cli-vX.Y.Z-linux-x64-standalone.zip`_also for the Windows Subsystem for Linux_ `balena-cli-vX.Y.Z-linux-x64-standalone.tar.gz`_also for the Windows Subsystem for Linux_
`balena-cli-vX.Y.Z-macOS-x64-standalone.zip` `balena-cli-vX.Y.Z-macOS-x64-standalone.tar.gz`
`balena-cli-vX.Y.Z-windows-x64-standalone.zip` `balena-cli-vX.Y.Z-windows-x64-standalone.tar.gz`
2. Extract the zip file contents to any folder you choose. The extracted contents will include a 2. Extract the tar.gz file contents to any folder you choose. The extracted contents will be a `balena` folder containing a `bin` subdirectory.
`balena-cli` folder.
3. Add the `balena-cli` folder to the system's `PATH` environment variable. 3. Add the `balena/bin` folder to the system's `PATH` environment variable.
See instructions for: See instructions for:
[Linux](https://stackoverflow.com/questions/14637979/how-to-permanently-set-path-on-linux-unix) | [Linux](https://stackoverflow.com/questions/14637979/how-to-permanently-set-path-on-linux-unix) |
[macOS](https://www.architectryan.com/2012/10/02/add-to-the-path-on-mac-os-x-mountain-lion/#.Uydjga1dXDg) | [macOS](https://www.architectryan.com/2012/10/02/add-to-the-path-on-mac-os-x-mountain-lion/#.Uydjga1dXDg) |
@ -61,14 +60,14 @@ macOS: | `/usr/local/src/balena-cli/` <br> `/usr/local/bin/balena`
> * If you are using macOS 10.15 or later (Catalina, Big Sur), [check this known issue and > * If you are using macOS 10.15 or later (Catalina, Big Sur), [check this known issue and
> workaround](https://github.com/balena-io/balena-cli/issues/2244). > workaround](https://github.com/balena-io/balena-cli/issues/2244).
> * **Linux Alpine** and **Busybox:** the standalone zip package is not currently compatible with > * **Linux Alpine** and **Busybox:** the standalone tar.gz package is not currently compatible with
> these "compact" Linux distributions, because of the alternative C libraries they ship with. > these "compact" Linux distributions, because of the alternative C libraries they ship with.
> For these, consider the [NPM Installation](#npm-installation) option. > For these, consider the [NPM Installation](#npm-installation) option.
> * Note that moving the `balena` executable out of the extracted `balena-cli` folder on its own > * Note that moving the `balena/bin/balena` executable out of the extracted `balena` folder on its own
> (e.g. moving it to `/usr/local/bin/balena`) will **not** work, as it depends on the other > (e.g. moving it to `/usr/local/bin/balena`) will **not** work, as it depends on the other
> folders and files also present in the `balena-cli` folder. > folders and files also present in the `balena` folder.
To update the CLI to a new version, download a new release zip file and replace the previous To update the CLI to a new version, download a new release tar.gz file and replace the previous
installation folder. To uninstall, simply delete the folder and edit the PATH environment variable installation folder. To uninstall, simply delete the folder and edit the PATH environment variable
as described above. as described above.
@ -78,8 +77,8 @@ If you are a Node.js developer, you may wish to install the balena CLI via [npm]
The npm installation involves building native (platform-specific) binary modules, which require The npm installation involves building native (platform-specific) binary modules, which require
some development tools to be installed first, as follows. some development tools to be installed first, as follows.
> **The balena CLI currently requires Node.js version ^20.6.0** > **The balena CLI currently requires Node.js version >=20.6.0**
> **Versions 21 and later are not yet fully supported.** > **Versions 23 and later are not yet fully supported.**
### Install development tools ### Install development tools
@ -89,7 +88,7 @@ some development tools to be installed first, as follows.
$ sudo apt-get update && sudo apt-get -y install curl python3 git make g++ $ sudo apt-get update && sudo apt-get -y install curl python3 git make g++
$ curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.38.0/install.sh | bash $ curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.38.0/install.sh | bash
$ . ~/.bashrc $ . ~/.bashrc
$ nvm install 20 $ nvm install 22
``` ```
The `curl` command line above uses The `curl` command line above uses
@ -106,7 +105,7 @@ recommended.
```sh ```sh
$ curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.38.0/install.sh | bash $ curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.38.0/install.sh | bash
$ . ~/.bashrc $ . ~/.bashrc
$ nvm install 20 $ nvm install 22
``` ```
#### **Windows** (not WSL) #### **Windows** (not WSL)
@ -114,7 +113,7 @@ $ nvm install 20
Install: Install:
* If you'd like the ability to switch between Node.js versions, install * If you'd like the ability to switch between Node.js versions, install
- Node.js v20 from the [Nodejs.org releases page](https://nodejs.org/en/download/releases/). - Node.js v22 from the [Nodejs.org releases page](https://nodejs.org/en/download/releases/).
[nvm-windows](https://github.com/coreybutler/nvm-windows#node-version-manager-nvm-for-windows) [nvm-windows](https://github.com/coreybutler/nvm-windows#node-version-manager-nvm-for-windows)
instead. instead.
* The [MSYS2 shell](https://www.msys2.org/), which provides `git`, `make`, `g++` and more: * The [MSYS2 shell](https://www.msys2.org/), which provides `git`, `make`, `g++` and more:

View File

@ -8,15 +8,15 @@ method.
Selected operating system: **Linux** Selected operating system: **Linux**
1. Download the latest zip file from the [latest release 1. Download the latest tar.gz file from the [latest release
page](https://github.com/balena-io/balena-cli/releases/latest). Look for a file name that ends page](https://github.com/balena-io/balena-cli/releases/latest). Look for a file name that ends
with "-standalone.zip", for example: with "-standalone.tar.gz", for example:
`balena-cli-vX.Y.Z-linux-x64-standalone.zip` `balena-cli-vX.Y.Z-linux-x64-standalone.tar.gz`
2. Extract the zip file contents to any folder you choose, for example `/home/james`. 2. Extract the tar.gz file contents to any folder you choose, for example `/home/james`.
The extracted contents will include a `balena-cli` folder. The extracted contents will include a `balena/bin` folder.
3. Add that folder (e.g. `/home/james/balena-cli`) to the `PATH` environment variable. 3. Add that folder (e.g. `/home/james/balena/bin`) to the `PATH` environment variable.
Check this [StackOverflow Check this [StackOverflow
post](https://stackoverflow.com/questions/14637979/how-to-permanently-set-path-on-linux-unix) post](https://stackoverflow.com/questions/14637979/how-to-permanently-set-path-on-linux-unix)
for instructions. Close and reopen the terminal window so that the changes to `PATH` for instructions. Close and reopen the terminal window so that the changes to `PATH`
@ -27,7 +27,7 @@ Selected operating system: **Linux**
* `balena version` - should print the CLI's version * `balena version` - should print the CLI's version
* `balena help` - should print a list of available commands * `balena help` - should print a list of available commands
To update the balena CLI to a new version, download a new release zip file and replace the previous To update the balena CLI to a new version, download a new release tar.gz file and replace the previous
installation folder. To uninstall, simply delete the folder and edit the PATH environment variable installation folder. To uninstall, simply delete the folder and edit the PATH environment variable
as described above. as described above.

View File

@ -7,8 +7,8 @@ Selected operating system: **macOS**
1. Download the installer from the [latest release 1. Download the installer from the [latest release
page](https://github.com/balena-io/balena-cli/releases/latest). page](https://github.com/balena-io/balena-cli/releases/latest).
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 on the downloaded file to run the installer and follow the installer's 2. Double click on the downloaded file to run the installer and follow the installer's
instructions. instructions.

View File

@ -8,7 +8,7 @@ Selected operating system: **Windows**
1. Download the installer from the [latest release 1. Download the installer from the [latest release
page](https://github.com/balena-io/balena-cli/releases/latest). page](https://github.com/balena-io/balena-cli/releases/latest).
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 on the downloaded file to run the installer and follow the installer's 2. Double click on the downloaded file to run the installer and follow the installer's
instructions. instructions.

45
MIGRATIONS.md Normal file
View File

@ -0,0 +1,45 @@
## Migrating to balena CLI v22
This guide outlines the changes introduced in balena CLI v22 and provides instructions for when and how to migrate.
---
### For Installer Users (Windows .exe, macOS .pkg)
If you are using the Windows executable (.exe) or macOS package (.pkg) installers, **no changes** are required for this update. You can continue to use the installers as before.
---
### For npm Installation Users
If you installed balena CLI via npm, **no changes** are required for this update. Your existing installation and update process remains the same.
---
### For Standalone Installation Users
Users of the standalone balena CLI will need to make the following adjustments:
1. **Archive Format Change**: The distribution archive format has changed from `.zip` to `.tar.gz`. You will need to use the `tar` command instead of `unzip` to extract the CLI.
* **Previous command (v21.x.x and older):**
```bash
unzip balena-cli-v21.1.12-linux-x64-standalone.zip
```
* **New command (v22.0.0 and newer):**
```bash
tar -xzf balena-cli-v22.0.0-linux-x64-standalone.tar.gz
```
2. **Executable Path Change**: The path to the balena CLI executable within the extracted folder has been updated.
* **Previous path (v21.x.x and older):**
```
balena-cli/balena
```
* **New path (v22.0.0 and newer):**
```
balena/bin/balena
```
Please update your scripts and any aliases to reflect these changes if you are using the standalone version.

View File

@ -20,6 +20,8 @@ GitHub](https://github.com/balena-io/balena-cli/), and your contribution is also
Check the [balena CLI installation instructions on Check the [balena CLI installation instructions on
GitHub](https://github.com/balena-io/balena-cli/blob/master/INSTALL.md). GitHub](https://github.com/balena-io/balena-cli/blob/master/INSTALL.md).
Note for v22 Migration: If you are upgrading to balena CLI v22 from a previous standalone installation, please [see the v22 Migration Guide](https://github.com/balena-io/balena-cli/blob/master/MIGRATIONS.md) for installation changes.
## Choosing a shell (command prompt/terminal) ## Choosing a shell (command prompt/terminal)
On **Windows,** the standard Command Prompt (`cmd.exe`) and On **Windows,** the standard Command Prompt (`cmd.exe`) and

View File

@ -115,7 +115,7 @@ If nothing seems to help, consider also using a different client-side terminal a
## "Docker seems to be unavailable" error when using Windows Subsystem for Linux (WSL) ## "Docker seems to be unavailable" error when using Windows Subsystem for Linux (WSL)
When running on WSL, the recommendation is to install a CLI release for Linux, like the standalone When running on WSL, the recommendation is to install a CLI release for Linux, like the standalone
zip package for Linux. However, commands like "balena build" will, by default, attempt to reach the tar.gz package for Linux. However, commands like "balena build" will, by default, attempt to reach the
Docker daemon at the Unix socket path `/var/run/docker.sock`, while Docker Desktop for Windows uses Docker daemon at the Unix socket path `/var/run/docker.sock`, while Docker Desktop for Windows uses
a Windows named pipe at `//./pipe/docker_engine` (which the Linux CLI on WSL cannot use). A a Windows named pipe at `//./pipe/docker_engine` (which the Linux CLI on WSL cannot use). A
solution is: solution is:

View File

@ -15,29 +15,17 @@
* limitations under the License. * limitations under the License.
*/ */
import type { JsonVersions } from '../src/commands/version/index';
import { run as oclifRun } from '@oclif/core'; import { run as oclifRun } from '@oclif/core';
import * as archiver from 'archiver';
import { exec, execFile } from 'child_process'; import { exec, execFile } from 'child_process';
import * as filehound from 'filehound';
import type { Stats } from 'fs'; import type { Stats } from 'fs';
import * as fs from 'fs-extra'; import * as fs from 'fs-extra';
import * as klaw from 'klaw'; import * as klaw from 'klaw';
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 { promisify } from 'util'; import { promisify } from 'util';
import { notarize } from '@electron/notarize'; import { notarize } from '@electron/notarize';
import { stripIndent } from '../build/utils/lazy'; import { loadPackageJson, ROOT, whichSpawn } from './utils';
import {
diffLines,
loadPackageJson,
ROOT,
StdOutTap,
whichSpawn,
} from './utils';
const execFileAsync = promisify(execFile); const execFileAsync = promisify(execFile);
const execAsync = promisify(exec); const execAsync = promisify(exec);
@ -55,12 +43,6 @@ interface PathByPlatform {
[platform: string]: string; [platform: string]: string;
} }
const standaloneZips: PathByPlatform = {
linux: dPath(`balena-cli-${version}-linux-${arch}-standalone.zip`),
darwin: dPath(`balena-cli-${version}-macOS-${arch}-standalone.zip`),
win32: dPath(`balena-cli-${version}-windows-${arch}-standalone.zip`),
};
const getOclifInstallersOriginalNames = async (): Promise<PathByPlatform> => { const getOclifInstallersOriginalNames = async (): Promise<PathByPlatform> => {
const { stdout } = await execAsync('git rev-parse --short HEAD'); const { stdout } = await execAsync('git rev-parse --short HEAD');
const sha = stdout.trim(); const sha = stdout.trim();
@ -75,260 +57,28 @@ const renamedOclifInstallers: PathByPlatform = {
win32: dPath(`balena-cli-${version}-windows-${arch}-installer.exe`), win32: dPath(`balena-cli-${version}-windows-${arch}-installer.exe`),
}; };
export const finalReleaseAssets: { [platform: string]: string[] } = { const getOclifStandaloneOriginalNames = async (): Promise<PathByPlatform> => {
win32: [standaloneZips['win32'], renamedOclifInstallers['win32']], const { stdout } = await execAsync('git rev-parse --short HEAD');
darwin: [standaloneZips['darwin'], renamedOclifInstallers['darwin']], const sha = stdout.trim();
linux: [standaloneZips['linux']], return {
linux: dPath(`balena-${version}-${sha}-linux-${arch}.tar.gz`),
darwin: dPath(`balena-${version}-${sha}-darwin-${arch}.tar.gz`),
win32: dPath(`balena-${version}-${sha}-win32-${arch}.tar.gz`),
};
}; };
/** const renamedOclifStandalone: PathByPlatform = {
* Given the output of `pkg` as a string (containing warning messages), linux: dPath(`balena-cli-${version}-linux-${arch}-standalone.tar.gz`),
* diff it against previously saved output of known "safe" warnings. darwin: dPath(`balena-cli-${version}-macOS-${arch}-standalone.tar.gz`),
* Throw an error if the diff is not empty. win32: dPath(`balena-cli-${version}-windows-${arch}-standalone.tar.gz`),
*/ };
async function diffPkgOutput(pkgOut: string) {
const { monochrome } = await import('../tests/helpers');
const relSavedPath = path.join(
'tests',
'test-data',
'pkg',
`expected-warnings-${process.platform}-${arch}.txt`,
);
const absSavedPath = path.join(ROOT, relSavedPath);
const ignoreStartsWith = [
'> pkg@',
'> Fetching base Node.js binaries',
' fetched-',
'prebuild-install WARN install No prebuilt binaries found',
];
const modulesRE =
process.platform === 'win32'
? /(?<=[ '])([A-Z]:)?\\.+?\\node_modules(?=\\)/
: /(?<=[ '])\/.+?\/node_modules(?=\/)/;
const buildRE =
process.platform === 'win32'
? /(?<=[ '])([A-Z]:)?\\.+\\build(?=\\)/
: /(?<=[ '])\/.+\/build(?=\/)/;
const cleanLines = (chunks: string | string[]) => {
const lines = typeof chunks === 'string' ? chunks.split('\n') : chunks;
return lines
.map((line: string) => monochrome(line)) // remove ASCII colors
.filter((line: string) => !/^\s*$/.test(line)) // blank lines
.filter((line: string) =>
ignoreStartsWith.every((i) => !line.startsWith(i)),
)
.map((line: string) => {
// replace absolute paths with relative paths
let replaced = line.replace(modulesRE, 'node_modules');
if (replaced === line) {
replaced = line.replace(buildRE, 'build');
}
return replaced;
});
};
pkgOut = cleanLines(pkgOut).join('\n');
const { readFile } = (await import('fs')).promises;
const expectedOut = cleanLines(await readFile(absSavedPath, 'utf8')).join(
'\n',
);
if (expectedOut !== pkgOut) {
const sep =
'================================================================================';
const diff = diffLines(expectedOut, pkgOut);
const msg = `pkg output does not match expected output from "${relSavedPath}"
Diff:
${sep}
${diff}
${sep}
Check whether the new or changed pkg warnings are safe to ignore, then update
"${relSavedPath}"
and share the result of your investigation as comments on the pull request.
Hint: the fix is often a matter of updating the 'pkg.scripts' or 'pkg.assets'
sections in the CLI's 'package.json' file, or a matter of updating the
'buildPkg' function in 'automation/build-bin.ts'. Sometimes it requires
patching dependencies: See for example 'patches/all/open+7.0.2.patch'.
${sep}
`;
throw new Error(msg);
}
}
/**
* Call `pkg.exec` to generate the standalone zip file, capturing its warning
* messages (stdout and stderr) in order to call diffPkgOutput().
*/
async function execPkg(...args: any[]) {
const { exec: pkgExec } = await import('@yao-pkg/pkg');
const outTap = new StdOutTap(true);
try {
outTap.tap();
await (pkgExec as any)(...args);
} catch (err) {
outTap.untap();
console.log(outTap.stdoutBuf.join(''));
console.error(outTap.stderrBuf.join(''));
throw err;
}
outTap.untap();
await diffPkgOutput(outTap.allBuf.join(''));
}
/**
* Use the 'pkg' module to create a single large executable file with
* the contents of 'node_modules' and the CLI's javascript code.
* Also copy a number of native modules (binary '.node' files) that are
* compiled during 'npm install' to the 'build-bin' folder, alongside
* the single large executable file created by pkg. (This is necessary
* because of a pkg limitation that does not allow binary executables
* to be directly executed from inside another binary executable.)
*/
async function buildPkg() {
// https://github.com/vercel/pkg#targets
let targets = `linux-${arch}`;
if (process.platform === 'darwin') {
targets = `macos-${arch}`;
}
// TBC: not yet possible to build for Windows arm64 on x64 nodes
if (process.platform === 'win32') {
targets = `win-x64`;
}
const args = [
'--targets',
targets,
'--output',
'build-bin/balena',
'package.json',
];
console.log('=======================================================');
console.log(`execPkg ${args.join(' ')}`);
console.log(`cwd="${process.cwd()}" ROOT="${ROOT}"`);
console.log('=======================================================');
await execPkg(args);
const paths: Array<[string, string[], string[]]> = [
// [platform, [source path], [destination path]]
['*', ['open', 'xdg-open'], ['xdg-open']],
['darwin', ['denymount', 'bin', 'denymount'], ['denymount']],
];
await Promise.all(
paths.map(([platform, source, dest]) => {
if (platform === '*' || platform === process.platform) {
// eg copy from node_modules/open/xdg-open to build-bin/xdg-open
return fs.copy(
path.join(ROOT, 'node_modules', ...source),
path.join(ROOT, 'build-bin', ...dest),
);
}
}),
);
const nativeExtensionPaths: string[] = await filehound
.create()
.paths(path.join(ROOT, 'node_modules'))
.ext(['node', 'dll'])
.find();
console.log(`\nCopying to build-bin:\n${nativeExtensionPaths.join('\n')}`);
await Promise.all(
nativeExtensionPaths.map((extPath) =>
fs.copy(
extPath,
extPath.replace(
path.join(ROOT, 'node_modules'),
path.join(ROOT, 'build-bin'),
),
),
),
);
}
/**
* Run some basic tests on the built pkg executable.
* TODO: test more than just `balena version -j`; integrate with the
* existing mocha/chai CLI command testing.
*/
async function testPkg() {
const pkgBalenaPath = path.join(
ROOT,
'build-bin',
process.platform === 'win32' ? 'balena.exe' : 'balena',
);
console.log(`Testing standalone package "${pkgBalenaPath}"...`);
// Run `balena version -j`, parse its stdout as JSON, and check that the
// reported Node.js major version matches semver.major(process.version)
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 pkgNodeMajorVersion = 0;
try {
const balenaVersions: JsonVersions = JSON.parse(stdout);
pkgNodeVersion = balenaVersions['Node.js'];
pkgNodeMajorVersion = semver.major(pkgNodeVersion);
} catch (err) {
throw new Error(stripIndent`
Error parsing JSON output of "balena version -j": ${err}
Original output: "${stdout}"`);
}
if (semver.major(process.version) !== pkgNodeMajorVersion) {
throw new Error(
`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)');
}
/**
* Create the zip file for the standalone 'pkg' bundle previously created
* by the buildPkg() function in 'build-bin.ts'.
*/
async function zipPkg() {
const outputFile = standaloneZips[process.platform];
if (!outputFile) {
throw new Error(
`Standalone installer unavailable for platform "${process.platform}"`,
);
}
await fs.mkdirp(path.dirname(outputFile));
await new Promise((resolve, reject) => {
console.log(`Zipping standalone package to "${outputFile}"...`);
const archive = archiver('zip', {
zlib: { level: 7 },
});
archive.directory(path.join(ROOT, 'build-bin'), 'balena-cli');
const outputStream = fs.createWriteStream(outputFile);
outputStream.on('close', resolve);
outputStream.on('error', reject);
archive.on('error', reject);
archive.on('warning', console.warn);
archive.pipe(outputStream);
archive.finalize().catch(reject);
});
}
export async function signFilesForNotarization() { export async function signFilesForNotarization() {
console.log('Signing files for notarization'); console.log('Signing files for notarization');
if (process.platform !== 'darwin') { // If signFilesForNotarization is called on the test CI environment (which will not set CSC_LINK)
// then we skip the signing process.
if (process.platform !== 'darwin' || !process.env.CSC_LINK) {
console.log('Skipping signing for notarization');
return; return;
} }
console.log('Deleting unneeded zip files...'); console.log('Deleting unneeded zip files...');
@ -416,20 +166,39 @@ export async function signFilesForNotarization() {
]); ]);
} }
export async function buildStandaloneZip() { export async function buildStandalone() {
console.log(`Building standalone zip package for CLI ${version}`); console.log(`Building standalone tarball for CLI ${version}`);
fs.rmSync('./tmp', { recursive: true, force: true });
fs.rmSync('./dist', { recursive: true, force: true });
fs.mkdirSync('./dist');
try { try {
await buildPkg(); let packOpts = ['-r', ROOT, '--no-xz'];
await testPkg(); if (process.platform === 'darwin') {
await zipPkg(); packOpts = packOpts.concat('--targets', `darwin-${arch}`);
console.log(`Standalone zip package build completed`); } else if (process.platform === 'win32') {
packOpts = packOpts.concat('--targets', 'win32-x64');
} else if (process.platform === 'linux') {
packOpts = packOpts.concat('--targets', `linux-${arch}`);
}
console.log(`Building oclif installer for CLI ${version}`);
const packCmd = `pack:tarballs`;
console.log('=======================================================');
console.log(`oclif ${packCmd} ${packOpts.join(' ')}`);
console.log(`cwd="${process.cwd()}" ROOT="${ROOT}"`);
console.log('=======================================================');
const oclifPath = path.join(ROOT, 'node_modules', 'oclif');
await oclifRun([packCmd].concat(...packOpts), oclifPath);
await renameStandalone();
console.log(`Standalone tarball package build completed`);
} catch (error) { } catch (error) {
console.error(`Error creating or testing standalone zip package`); console.error(`Error creating or testing standalone tarball package`);
throw error; throw error;
} }
} }
async function renameInstallerFiles() { async function renameInstallers() {
const oclifInstallers = await getOclifInstallersOriginalNames(); const oclifInstallers = await getOclifInstallersOriginalNames();
if (await fs.pathExists(oclifInstallers[process.platform])) { if (await fs.pathExists(oclifInstallers[process.platform])) {
await fs.rename( await fs.rename(
@ -439,6 +208,16 @@ async function renameInstallerFiles() {
} }
} }
async function renameStandalone() {
const oclifStandalone = await getOclifStandaloneOriginalNames();
if (await fs.pathExists(oclifStandalone[process.platform])) {
await fs.rename(
oclifStandalone[process.platform],
renamedOclifStandalone[process.platform],
);
}
}
/** /**
* If the CSC_LINK and CSC_KEY_PASSWORD env vars are set, digitally sign the * If the CSC_LINK and CSC_KEY_PASSWORD env vars are set, digitally sign the
* executable installer using Microsoft SignTool.exe (Sign Tool) * executable installer using Microsoft SignTool.exe (Sign Tool)
@ -446,7 +225,7 @@ async function renameInstallerFiles() {
*/ */
async function signWindowsInstaller() { async function signWindowsInstaller() {
if (process.env.SM_CODE_SIGNING_CERT_SHA1_HASH) { if (process.env.SM_CODE_SIGNING_CERT_SHA1_HASH) {
const exeName = renamedOclifInstallers[process.platform]; const exeName = (await getOclifInstallersOriginalNames())[process.platform];
console.log(`Signing installer "${exeName}"`); console.log(`Signing installer "${exeName}"`);
// trust ... // trust ...
await execFileAsync('signtool.exe', [ await execFileAsync('signtool.exe', [
@ -480,12 +259,14 @@ async function notarizeMacInstaller(): Promise<void> {
const appleId = const appleId =
process.env.XCODE_APP_LOADER_EMAIL || 'accounts+apple@balena.io'; process.env.XCODE_APP_LOADER_EMAIL || 'accounts+apple@balena.io';
const appleIdPassword = process.env.XCODE_APP_LOADER_PASSWORD; const appleIdPassword = process.env.XCODE_APP_LOADER_PASSWORD;
const appPath = (await getOclifInstallersOriginalNames())[process.platform];
console.log(`Notarizing file "${appPath}"`);
if (appleIdPassword && teamId) { if (appleIdPassword && teamId) {
await notarize({ await notarize({
tool: 'notarytool', tool: 'notarytool',
teamId, teamId,
appPath: renamedOclifInstallers.darwin, appPath,
appleId, appleId,
appleIdPassword, appleIdPassword,
}); });
@ -525,7 +306,6 @@ export async function buildOclifInstaller() {
console.log('======================================================='); console.log('=======================================================');
const oclifPath = path.join(ROOT, 'node_modules', 'oclif'); const oclifPath = path.join(ROOT, 'node_modules', 'oclif');
await oclifRun([packCmd].concat(...packOpts), oclifPath); await oclifRun([packCmd].concat(...packOpts), oclifPath);
await renameInstallerFiles();
// The Windows installer is explicitly signed here (oclif doesn't do it). // The Windows installer is explicitly signed here (oclif doesn't do it).
// The macOS installer is automatically signed by oclif (which runs the // The macOS installer is automatically signed by oclif (which runs the
// `pkgbuild` tool), using the certificate name given in package.json // `pkgbuild` tool), using the certificate name given in package.json
@ -537,6 +317,7 @@ export async function buildOclifInstaller() {
await notarizeMacInstaller(); // Notarize await notarizeMacInstaller(); // Notarize
console.log('Package notarized.'); console.log('Package notarized.');
} }
await renameInstallers();
console.log(`oclif installer build completed`); console.log(`oclif installer build completed`);
} }
} }
@ -572,4 +353,5 @@ export async function testShrinkwrap(): Promise<void> {
if (process.platform !== 'win32') { if (process.platform !== 'win32') {
await whichSpawn(path.resolve(__dirname, 'test-lock-deduplicated.sh')); await whichSpawn(path.resolve(__dirname, 'test-lock-deduplicated.sh'));
} }
await Promise.resolve();
} }

View File

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

View File

@ -19,7 +19,7 @@ import * as _ from 'lodash';
import { import {
buildOclifInstaller, buildOclifInstaller,
buildStandaloneZip, buildStandalone,
catchUncommitted, catchUncommitted,
signFilesForNotarization, signFilesForNotarization,
testShrinkwrap, testShrinkwrap,
@ -36,7 +36,7 @@ process.env.DEBUG = ['0', 'no', 'false', '', undefined].includes(
* 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:
* 'build:installer' (to build a native oclif installer) * 'build:installer' (to build a native oclif installer)
* 'build:standalone' (to build a standalone pkg package) * 'build:standalone' (to build a standalone package)
* *
* @param args Arguments to parse (default is process.argv.slice(2)) * @param args Arguments to parse (default is process.argv.slice(2))
*/ */
@ -49,7 +49,7 @@ async function parse(args?: string[]) {
} }
const commands: { [cmd: string]: () => void | Promise<void> } = { const commands: { [cmd: string]: () => void | Promise<void> } = {
'build:installer': buildOclifInstaller, 'build:installer': buildOclifInstaller,
'build:standalone': buildStandaloneZip, 'build:standalone': buildStandalone,
'sign:binaries': signFilesForNotarization, 'sign:binaries': signFilesForNotarization,
'catch-uncommitted': catchUncommitted, 'catch-uncommitted': catchUncommitted,
'test-shrinkwrap': testShrinkwrap, 'test-shrinkwrap': testShrinkwrap,

View File

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

View File

@ -18,72 +18,10 @@
import { spawn } from 'child_process'; import { spawn } from 'child_process';
import * as path from 'path'; import * as path from 'path';
import * as fs from 'fs'; import * as fs from 'fs';
import { diffTrimmedLines } from 'diff'; import * as whichMod from 'which';
export const ROOT = path.join(__dirname, '..'); export const ROOT = path.join(__dirname, '..');
/** Tap and buffer this process' stdout and stderr */
export class StdOutTap {
public stdoutBuf: string[] = [];
public stderrBuf: string[] = [];
public allBuf: string[] = []; // both stdout and stderr
protected origStdoutWrite: typeof process.stdout.write;
protected origStderrWrite: typeof process.stdout.write;
constructor(protected printDots = false) {}
tap() {
this.origStdoutWrite = process.stdout.write;
this.origStderrWrite = process.stderr.write;
process.stdout.write = (chunk: string, ...args: any[]): boolean => {
this.stdoutBuf.push(chunk);
this.allBuf.push(chunk);
const str = this.printDots ? '.' : chunk;
return this.origStdoutWrite.call(process.stdout, str, ...args);
};
process.stderr.write = (chunk: string, ...args: any[]): boolean => {
this.stderrBuf.push(chunk);
this.allBuf.push(chunk);
const str = this.printDots ? '.' : chunk;
return this.origStderrWrite.call(process.stderr, str, ...args);
};
}
untap() {
process.stdout.write = this.origStdoutWrite;
process.stderr.write = this.origStderrWrite;
if (this.printDots) {
console.error('');
}
}
}
/**
* Diff strings by line, using the 'diff' npm package:
* https://www.npmjs.com/package/diff
*/
export function diffLines(str1: string, str2: string): string {
const diffObjs = diffTrimmedLines(str1, str2);
const prefix = (chunk: string, char: string) =>
chunk
.split('\n')
.map((line: string) => `${char} ${line}`)
.join('\n');
const diffStr = diffObjs
.map((part: any) => {
return part.added
? prefix(part.value, '+')
: part.removed
? prefix(part.value, '-')
: prefix(part.value, ' ');
})
.join('\n');
return diffStr;
}
export function loadPackageJson() { export function loadPackageJson() {
const packageJsonPath = path.join(ROOT, 'package.json'); const packageJsonPath = path.join(ROOT, 'package.json');
@ -101,7 +39,6 @@ export function loadPackageJson() {
* @returns The program's full path, e.g. 'C:\WINDOWS\System32\OpenSSH\ssh.EXE' * @returns The program's full path, e.g. 'C:\WINDOWS\System32\OpenSSH\ssh.EXE'
*/ */
export async function which(program: string): Promise<string> { export async function which(program: string): Promise<string> {
const whichMod = await import('which');
let programPath: string; let programPath: string;
try { try {
programPath = await whichMod(program); programPath = await whichMod(program);
@ -132,7 +69,7 @@ export async function whichSpawn(
.on('error', reject) .on('error', reject)
.on('close', resolve); .on('close', resolve);
} catch (err) { } catch (err) {
reject(err); reject(err as Error);
} }
}); });
} catch (err) { } catch (err) {

View File

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

View File

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

View File

@ -13,6 +13,8 @@ GitHub](https://github.com/balena-io/balena-cli/), and your contribution is also
Check the [balena CLI installation instructions on Check the [balena CLI installation instructions on
GitHub](https://github.com/balena-io/balena-cli/blob/master/INSTALL.md). GitHub](https://github.com/balena-io/balena-cli/blob/master/INSTALL.md).
Note for v22 Migration: If you are upgrading to balena CLI v22 from a previous standalone installation, please [see the v22 Migration Guide](https://github.com/balena-io/balena-cli/blob/master/MIGRATIONS.md) for installation changes.
## Choosing a shell (command prompt/terminal) ## Choosing a shell (command prompt/terminal)
On **Windows,** the standard Command Prompt (`cmd.exe`) and On **Windows,** the standard Command Prompt (`cmd.exe`) and
@ -326,6 +328,8 @@ or to authenticate requests to the API with an 'Authorization: Bearer <key>' hea
Examples: Examples:
$ balena api-key generate "Jenkins Key" $ balena api-key generate "Jenkins Key"
$ balena api-key generate "Jenkins Key" 2025-10-30
$ balena api-key generate "Jenkins Key" never
### Arguments ### Arguments
@ -333,6 +337,10 @@ Examples:
the API key name the API key name
#### EXPIRYDATE
the expiry date of the API key as an ISO date string, or "never" for no expiry
## api-key list ## api-key list
### Aliases ### Aliases
@ -568,9 +576,9 @@ Generate a config.json file for a device or fleet.
The target balenaOS version must be specified with the --version option. The target balenaOS version must be specified with the --version option.
The '--dev' option is used to configure balenaOS to operate in development mode, The '--dev' option is used to configure balenaOS to operate in development mode,
allowing anauthenticated root ssh access and exposing network ports such as allowing unauthenticated root ssh access and exposing network ports such as
balenaEngine's 2375 (unencrypted). This option causes `"developmentMode": true` balenaEngine's 2375 (unencrypted). This option causes `"developmentMode": true`
to be inserted in the 'config.json' file in the image's boot partion. Development to be inserted in the 'config.json' file in the image's boot partition. Development
mode (as a configurable option) is applicable to balenaOS releases from early mode (as a configurable option) is applicable to balenaOS releases from early
2022. Older releases have separate development and production balenaOS images 2022. Older releases have separate development and production balenaOS images
that cannot be reconfigured through 'config.json' or the '--dev' option. Do not that cannot be reconfigured through 'config.json' or the '--dev' option. Do not
@ -2866,9 +2874,9 @@ The --device-type option is used to override the fleet's default device type,
in case of a fleet with mixed device types. in case of a fleet with mixed device types.
The '--dev' option is used to configure balenaOS to operate in development mode, The '--dev' option is used to configure balenaOS to operate in development mode,
allowing anauthenticated root ssh access and exposing network ports such as allowing unauthenticated root ssh access and exposing network ports such as
balenaEngine's 2375 (unencrypted). This option causes `"developmentMode": true` balenaEngine's 2375 (unencrypted). This option causes `"developmentMode": true`
to be inserted in the 'config.json' file in the image's boot partion. Development to be inserted in the 'config.json' file in the image's boot partition. Development
mode (as a configurable option) is applicable to balenaOS releases from early mode (as a configurable option) is applicable to balenaOS releases from early
2022. Older releases have separate development and production balenaOS images 2022. Older releases have separate development and production balenaOS images
that cannot be reconfigured through 'config.json' or the '--dev' option. Do not that cannot be reconfigured through 'config.json' or the '--dev' option. Do not

32
eslint.config.js Normal file
View File

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

8695
npm-shrinkwrap.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
{ {
"name": "balena-cli", "name": "balena-cli",
"version": "20.0.1", "version": "22.1.1",
"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",
@ -24,26 +24,6 @@
"bin": { "bin": {
"balena": "./bin/run.js" "balena": "./bin/run.js"
}, },
"pkg": {
"scripts": [
"build/**/*.js",
"node_modules/balena-sdk/es2018/index.js",
"node_modules/pinejs-client-request/node_modules/pinejs-client-core/es2018/index.js",
"node_modules/@balena/compose/dist/parse/schemas/*.json"
],
"assets": [
"build/auth/pages/*.ejs",
"node_modules/balena-sdk/node_modules/balena-pine/**/*",
"node_modules/balena-pine/**/*",
"node_modules/pinejs-client-core/**/*",
"node_modules/open/xdg-open",
"node_modules/windosu/*.bat",
"node_modules/windosu/*.cmd",
"node_modules/axios/**/*",
"npm-shrinkwrap.json",
"oclif.manifest.json"
]
},
"scripts": { "scripts": {
"postinstall": "node patches/apply-patches.js", "postinstall": "node patches/apply-patches.js",
"prebuild": "rimraf build/ build-bin/", "prebuild": "rimraf build/ build-bin/",
@ -58,6 +38,7 @@
"build:completion": "node completion/generate-completion.js", "build:completion": "node completion/generate-completion.js",
"build:standalone": "ts-node --transpile-only automation/run.ts build:standalone", "build:standalone": "ts-node --transpile-only automation/run.ts build:standalone",
"build:installer": "ts-node --transpile-only automation/run.ts build:installer", "build:installer": "ts-node --transpile-only automation/run.ts build:installer",
"deduplicate-dependencies": "npm dd && git add npm-shrinkwrap.json && git commit --message \"Deduplicate dependencies\"",
"package": "npm run build:fast && npm run build:standalone && npm run build:installer", "package": "npm run build:fast && npm run build:standalone && npm run build:installer",
"pretest": "npm run build", "pretest": "npm run build",
"test": "npm run test:shrinkwrap && npm run test:core", "test": "npm run test:shrinkwrap && npm run test:core",
@ -91,7 +72,7 @@
"author": "Balena Inc. (https://balena.io/)", "author": "Balena Inc. (https://balena.io/)",
"license": "Apache-2.0", "license": "Apache-2.0",
"engines": { "engines": {
"node": "^20.6.0" "node": ">=20.6.0 <23"
}, },
"oclif": { "oclif": {
"bin": "balena", "bin": "balena",
@ -111,16 +92,14 @@
} }
}, },
"devDependencies": { "devDependencies": {
"@balena/lint": "^8.0.0", "@balena/lint": "^9.1.3",
"@electron/notarize": "^2.0.0", "@electron/notarize": "^2.0.0",
"@types/archiver": "^6.0.2",
"@types/bluebird": "^3.5.36", "@types/bluebird": "^3.5.36",
"@types/body-parser": "^1.19.2", "@types/body-parser": "^1.19.2",
"@types/chai": "^4.3.0", "@types/chai": "^4.3.0",
"@types/chai-as-promised": "^7.1.4", "@types/chai-as-promised": "^7.1.4",
"@types/cli-truncate": "^2.0.0", "@types/cli-truncate": "^2.0.0",
"@types/common-tags": "^1.8.1", "@types/common-tags": "^1.8.1",
"@types/diff": "^5.0.3",
"@types/dockerode": "3.3.23", "@types/dockerode": "3.3.23",
"@types/ejs": "^3.1.0", "@types/ejs": "^3.1.0",
"@types/express": "^4.17.13", "@types/express": "^4.17.13",
@ -145,7 +124,6 @@
"@types/node-cleanup": "^2.1.2", "@types/node-cleanup": "^2.1.2",
"@types/prettyjson": "^0.0.33", "@types/prettyjson": "^0.0.33",
"@types/progress-stream": "^2.0.2", "@types/progress-stream": "^2.0.2",
"@types/request": "^2.48.7",
"@types/rewire": "^2.5.30", "@types/rewire": "^2.5.30",
"@types/rimraf": "^3.0.2", "@types/rimraf": "^3.0.2",
"@types/semver": "^7.3.9", "@types/semver": "^7.3.9",
@ -159,16 +137,12 @@
"@types/update-notifier": "^4.1.1", "@types/update-notifier": "^4.1.1",
"@types/which": "^2.0.1", "@types/which": "^2.0.1",
"@types/window-size": "^1.1.1", "@types/window-size": "^1.1.1",
"@yao-pkg/pkg": "^5.11.1",
"archiver": "^7.0.1",
"catch-uncommitted": "^2.0.0", "catch-uncommitted": "^2.0.0",
"chai": "^4.3.4", "chai": "^4.3.4",
"chai-as-promised": "^7.1.1", "chai-as-promised": "^7.1.1",
"cross-env": "^7.0.3", "cross-env": "^7.0.3",
"deep-object-diff": "^1.1.0", "deep-object-diff": "^1.1.0",
"diff": "^5.0.0",
"ent": "^2.2.0", "ent": "^2.2.0",
"filehound": "^1.17.5",
"fs-extra": "^11.2.0", "fs-extra": "^11.2.0",
"http-proxy": "^1.18.1", "http-proxy": "^1.18.1",
"husky": "^9.1.5", "husky": "^9.1.5",
@ -179,28 +153,28 @@
"mocha": "^10.6.0", "mocha": "^10.6.0",
"mock-fs": "^5.2.0", "mock-fs": "^5.2.0",
"mock-require": "^3.0.3", "mock-require": "^3.0.3",
"nock": "^13.2.1", "nock": "^14.0.4",
"oclif": "^4.14.0", "oclif": "^4.17.0",
"rewire": "^7.0.0", "rewire": "^7.0.0",
"simple-git": "^3.14.1", "simple-git": "^3.14.1",
"sinon": "^19.0.0", "sinon": "^19.0.0",
"string-to-stream": "^3.0.1", "string-to-stream": "^3.0.1",
"ts-node": "^10.4.0", "ts-node": "^10.4.0",
"typescript": "^5.6.2" "typescript": "^5.8.2"
}, },
"dependencies": { "dependencies": {
"@balena/compose": "^5.0.0", "@balena/compose": "^7.0.9",
"@balena/dockerignore": "^1.0.2", "@balena/dockerignore": "^1.0.2",
"@balena/env-parsing": "^1.1.8", "@balena/env-parsing": "^1.1.8",
"@balena/es-version": "^1.0.1", "@balena/es-version": "^1.0.1",
"@oclif/core": "^4.0.31", "@oclif/core": "^4.1.0",
"@sentry/node": "^6.16.1", "@sentry/node": "^9.0.0",
"balena-config-json": "^4.2.0", "balena-config-json": "^4.2.7",
"balena-device-init": "^7.0.1", "balena-device-init": "^8.1.11",
"balena-errors": "^4.7.3", "balena-errors": "^4.7.3",
"balena-image-fs": "^7.0.6", "balena-image-fs": "^7.5.2",
"balena-preload": "^16.0.0", "balena-preload": "^18.0.4",
"balena-sdk": "^20.3.0", "balena-sdk": "^21.3.0",
"balena-semver": "^2.3.0", "balena-semver": "^2.3.0",
"balena-settings-client": "^5.0.2", "balena-settings-client": "^5.0.2",
"balena-settings-storage": "^8.1.0", "balena-settings-storage": "^8.1.0",
@ -211,12 +185,13 @@
"cli-truncate": "^2.1.0", "cli-truncate": "^2.1.0",
"color-hash": "^1.1.1", "color-hash": "^1.1.1",
"common-tags": "^1.7.2", "common-tags": "^1.7.2",
"date-fns": "^4.1.0",
"denymount": "^2.3.0", "denymount": "^2.3.0",
"docker-modem": "^5.0.3", "docker-modem": "^5.0.6",
"docker-progress": "^5.1.3", "docker-progress": "^5.1.3",
"dockerode": "^4.0.2", "dockerode": "^4.0.5",
"ejs": "^3.1.6", "ejs": "^3.1.6",
"etcher-sdk": "9.1.0", "etcher-sdk": "^10.0.0",
"express": "^4.17.2", "express": "^4.17.2",
"fast-boot2": "^1.1.0", "fast-boot2": "^1.1.0",
"fast-levenshtein": "^3.0.0", "fast-levenshtein": "^3.0.0",
@ -231,6 +206,7 @@
"is-root": "^2.1.0", "is-root": "^2.1.0",
"js-yaml": "^4.1.0", "js-yaml": "^4.1.0",
"JSONStream": "^1.0.3", "JSONStream": "^1.0.3",
"jwt-decode": "^3.1.2",
"livepush": "^3.5.1", "livepush": "^3.5.1",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"mime": "^2.4.6", "mime": "^2.4.6",
@ -243,9 +219,8 @@
"prettyjson": "^1.2.5", "prettyjson": "^1.2.5",
"progress-stream": "^2.0.0", "progress-stream": "^2.0.0",
"reconfix": "^1.0.0-v0-1-0-fork-46760acff4d165f5238bfac5e464256ef1944476", "reconfix": "^1.0.0-v0-1-0-fork-46760acff4d165f5238bfac5e464256ef1944476",
"request": "^2.88.2", "resin-cli-form": "^4.0.0",
"resin-cli-form": "^3.0.0", "resin-cli-visuals": "^3.0.0",
"resin-cli-visuals": "^2.0.1",
"resin-doodles": "^0.2.0", "resin-doodles": "^0.2.0",
"resin-stream-logger": "^0.1.2", "resin-stream-logger": "^0.1.2",
"rimraf": "^3.0.2", "rimraf": "^3.0.2",
@ -273,6 +248,6 @@
} }
}, },
"versionist": { "versionist": {
"publishedAt": "2024-10-29T11:37:11.260Z" "publishedAt": "2025-06-19T09:32:53.877Z"
} }
} }

View File

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

View File

@ -1,16 +0,0 @@
diff --git a/node_modules/open/index.js b/node_modules/open/index.js
index 13147d0..ff161dd 100644
--- a/node_modules/open/index.js
+++ b/node_modules/open/index.js
@@ -10,7 +10,10 @@ const pAccess = promisify(fs.access);
const pReadFile = promisify(fs.readFile);
// Path to included `xdg-open`.
-const localXdgOpenPath = path.join(__dirname, 'xdg-open');
+const localXdgOpenPath = process.pkg
+ ? path.join(path.dirname(process.execPath), 'xdg-open')
+ : path.join(__dirname, 'xdg-open');
+
/**
Get the mount point for fixed drives in WSL.

View File

@ -1,14 +0,0 @@
diff --git a/node_modules/node-gyp-build/node-gyp-build.js b/node_modules/node-gyp-build/node-gyp-build.js
index 61b398e..3cc3be8 100644
--- a/node_modules/node-gyp-build/node-gyp-build.js
+++ b/node_modules/node-gyp-build/node-gyp-build.js
@@ -30,6 +30,9 @@ load.resolve = load.path = function (dir) {
if (process.env[name + '_PREBUILD']) dir = process.env[name + '_PREBUILD']
} catch (err) {}
+ // pkg fix: native node modules are located externally to the pkg executable
+ dir = dir.replace(/^\/snapshot\/.+?\/node_modules\//, path.dirname(process.execPath) + path.sep)
+
if (!prebuildsOnly) {
var release = getFirst(path.join(dir, 'build/Release'), matchBuild)
if (release) return release

View File

@ -1,38 +0,0 @@
diff --git a/node_modules/windosu/lib/pipe.js b/node_modules/windosu/lib/pipe.js
index dc81fa5..a381cc7 100644
--- a/node_modules/windosu/lib/pipe.js
+++ b/node_modules/windosu/lib/pipe.js
@@ -42,7 +42,8 @@ function pipe(path, options) {
return d.promise;
}
module.exports = pipe;
-if (module === require.main) {
+
+function main() {
if (!process.argv[4]) {
console.error('Incorrect arguments!');
process.exit(-1);
@@ -52,3 +53,8 @@ if (module === require.main) {
serve: process.argv[3] == 'server'
});
}
+module.exports.main = main;
+
+if (module === require.main) {
+ main();
+}
diff --git a/node_modules/windosu/lib/windosu.js b/node_modules/windosu/lib/windosu.js
index 6502812..dd0391a 100644
--- a/node_modules/windosu/lib/windosu.js
+++ b/node_modules/windosu/lib/windosu.js
@@ -16,7 +16,9 @@ module.exports.exec = function (command, options, callback) {
temp: temp,
command: command,
cliWidth: cliWidth(),
- pipe: '"' + process.execPath + '" "' + path.join(__dirname, 'pipe.js') + '"',
+ pipe: process.pkg
+ ? '"' + process.execPath + '" pkgExec "' + path.join(__dirname, 'pipe.js') + '::main"'
+ : '"' + process.execPath + '" "' + path.join(__dirname, 'pipe.js') + '"',
input: inputName = id + '-in',
output: outputName = id + '-out',
stderr_redir: process.stdout.isTTY ? '2>&1' : '2> %ERROR%'

View File

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

View File

@ -34,18 +34,14 @@ 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,
}); });
Sentry.configureScope((scope) => { Sentry.getCurrentScope().setExtras({
scope.setExtras({ is_pkg: !!(process as any).pkg,
is_pkg: !!(process as any).pkg, node_version: process.version,
node_version: process.version, platform: process.platform,
platform: process.platform,
});
}); });
return Sentry.getCurrentHub();
}); });
async function checkNodeVersion() { async function checkNodeVersion() {
@ -101,11 +97,9 @@ async function init() {
/** Execute the oclif parser and the CLI command. */ /** Execute the oclif parser and the CLI command. */
async function oclifRun(command: string[], options: AppOptions) { async function oclifRun(command: string[], options: AppOptions) {
let deprecationPromise: Promise<void>; let deprecationPromise: Promise<void> | undefined;
// check and enforce the CLI's deprecation policy // check and enforce the CLI's deprecation policy
if (unsupportedFlag || process.env.BALENARC_UNSUPPORTED) { if (!(unsupportedFlag || process.env.BALENARC_UNSUPPORTED)) {
deprecationPromise = Promise.resolve();
} else {
const { DeprecationChecker } = await import('./deprecation'); const { DeprecationChecker } = await import('./deprecation');
const deprecationChecker = new DeprecationChecker(packageJSON.version); const deprecationChecker = new DeprecationChecker(packageJSON.version);
// warnAndAbortIfDeprecated uses previously cached data only // warnAndAbortIfDeprecated uses previously cached data only
@ -161,18 +155,12 @@ async function oclifRun(command: string[], options: AppOptions) {
/** CLI entrypoint. Called by the `bin/run.js` and `bin/dev.js` scripts. */ /** CLI entrypoint. Called by the `bin/run.js` and `bin/dev.js` scripts. */
export async function run(cliArgs = process.argv, options: AppOptions) { export async function run(cliArgs = process.argv, options: AppOptions) {
try { try {
const { setOfflineModeEnvVars, normalizeEnvVars, pkgExec } = await import( const { setOfflineModeEnvVars, normalizeEnvVars } = await import(
'./utils/bootstrap' './utils/bootstrap'
); );
setOfflineModeEnvVars(); setOfflineModeEnvVars();
normalizeEnvVars(); normalizeEnvVars();
// The 'pkgExec' special/internal command provides a Node.js interpreter
// for use of the standalone zip package. See pkgExec function.
if (cliArgs.length > 3 && cliArgs[2] === 'pkgExec') {
return pkgExec(cliArgs[3], cliArgs.slice(4));
}
await init(); await init();
// 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

View File

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

View File

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

View File

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

View File

@ -64,7 +64,12 @@ export default class ConfigInjectCmd extends Command {
); );
const config = await import('balena-config-json'); const config = await import('balena-config-json');
await config.write(drive, '', configJSON); await config.write(
drive,
// Will be removed in the next major of balena-config-json
undefined,
configJSON,
);
console.info('Done'); console.info('Done');
} }

View File

@ -54,7 +54,7 @@ export default class ConfigReadCmd extends Command {
await safeUmount(drive); await safeUmount(drive);
const config = await import('balena-config-json'); const config = await import('balena-config-json');
const configJSON = await config.read(drive, ''); const configJSON = await config.read(drive);
if (options.json) { if (options.json) {
console.log(JSON.stringify(configJSON, null, 4)); console.log(JSON.stringify(configJSON, null, 4));

View File

@ -62,7 +62,7 @@ export default class ConfigReconfigureCmd extends Command {
await safeUmount(drive); await safeUmount(drive);
const config = await import('balena-config-json'); const config = await import('balena-config-json');
const { uuid } = await config.read(drive, ''); const { uuid } = await config.read(drive);
await safeUmount(drive); await safeUmount(drive);
if (!uuid) { if (!uuid) {

View File

@ -64,14 +64,19 @@ export default class ConfigWriteCmd extends Command {
await safeUmount(drive); await safeUmount(drive);
const config = await import('balena-config-json'); const config = await import('balena-config-json');
const configJSON = await config.read(drive, ''); const configJSON = await config.read(drive);
console.info(`Setting ${params.key} to ${params.value}`); console.info(`Setting ${params.key} to ${params.value}`);
ConfigWriteCmd.updateConfigJson(configJSON, params.key, params.value); ConfigWriteCmd.updateConfigJson(configJSON, params.key, params.value);
await denyMount(drive, async () => { await denyMount(drive, async () => {
await safeUmount(drive); await safeUmount(drive);
await config.write(drive, '', configJSON); await config.write(
drive,
// Will be removed in the next major of balena-config-json
undefined,
configJSON,
);
}); });
console.info('Done'); console.info('Done');

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -24,7 +24,7 @@ import { tryAsInteger } from '../../utils/validation';
import { jsonInfo } from '../../utils/messages'; import { jsonInfo } from '../../utils/messages';
export const commitOrIdArg = Args.custom({ export const commitOrIdArg = Args.custom({
parse: async (commitOrId: string) => tryAsInteger(commitOrId), parse: tryAsInteger,
}); });
type FlagsDef = Interfaces.InferredFlags<typeof ReleaseCmd.flags>; type FlagsDef = Interfaces.InferredFlags<typeof ReleaseCmd.flags>;
@ -86,7 +86,7 @@ export default class ReleaseCmd extends Command {
balena: BalenaSdk.BalenaSDK, balena: BalenaSdk.BalenaSDK,
options: FlagsDef, options: FlagsDef,
) { ) {
const fields: Array<keyof BalenaSdk.Release> = [ const fields = [
'id', 'id',
'commit', 'commit',
'created_at', 'created_at',
@ -96,7 +96,7 @@ export default class ReleaseCmd extends Command {
'build_log', 'build_log',
'start_timestamp', 'start_timestamp',
'end_timestamp', 'end_timestamp',
]; ] satisfies BalenaSdk.PineOptions<BalenaSdk.Release>['$select'];
const release = await balena.models.release.get(commitOrId, { const release = await balena.models.release.get(commitOrId, {
...(!options.json && { $select: fields }), ...(!options.json && { $select: fields }),

View File

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

View File

@ -16,7 +16,6 @@
*/ */
import { Args, Command } from '@oclif/core'; import { Args, Command } from '@oclif/core';
import { ExpectedError } from '../../errors';
import { getBalenaSdk, stripIndent } from '../../utils/lazy'; import { getBalenaSdk, stripIndent } from '../../utils/lazy';
export default class SSHKeyAddCmd extends Command { export default class SSHKeyAddCmd extends Command {
@ -59,6 +58,7 @@ export default class SSHKeyAddCmd extends Command {
}), }),
path: Args.string({ path: Args.string({
description: `the path to the public key file`, description: `the path to the public key file`,
required: true,
}), }),
}; };
@ -67,12 +67,12 @@ export default class SSHKeyAddCmd extends Command {
public async run() { public async run() {
const { args: params } = await this.parse(SSHKeyAddCmd); const { args: params } = await this.parse(SSHKeyAddCmd);
const { readFile } = (await import('fs')).promises;
let key: string; let key: string;
if (params.path != null) { try {
const { readFile } = (await import('fs')).promises;
key = await readFile(params.path, 'utf8'); key = await readFile(params.path, 'utf8');
} else { } catch {
throw new ExpectedError('No public key file or path provided.'); key = params.path;
} }
await getBalenaSdk().models.key.create(params.name, key); await getBalenaSdk().models.key.create(params.name, key);

View File

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

View File

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

View File

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

View File

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

View File

@ -38,11 +38,11 @@ import { stripIndent } from './utils/lazy';
export async function trackCommand(commandSignature: string) { export async function trackCommand(commandSignature: string) {
try { try {
let Sentry: typeof import('@sentry/node'); let Sentry: typeof import('@sentry/node');
let scope: import('@sentry/node').Scope;
if (!process.env.BALENARC_NO_SENTRY) { if (!process.env.BALENARC_NO_SENTRY) {
Sentry = await import('@sentry/node'); Sentry = await import('@sentry/node');
Sentry.configureScope((scope) => { scope = Sentry.getCurrentScope();
scope.setExtra('command', commandSignature); scope.setExtra('command', commandSignature);
});
} }
const { getCachedUsername } = await import('./utils/bootstrap'); const { getCachedUsername } = await import('./utils/bootstrap');
let username: string | undefined; let username: string | undefined;
@ -52,11 +52,9 @@ export async function trackCommand(commandSignature: string) {
// ignore // ignore
} }
if (!process.env.BALENARC_NO_SENTRY) { if (!process.env.BALENARC_NO_SENTRY) {
Sentry!.configureScope((scope) => { scope!.setUser({
scope.setUser({ id: username,
id: username, username,
username,
});
}); });
} }
if ( if (

View File

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

View File

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

View File

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

View File

@ -83,43 +83,6 @@ export function setOfflineModeEnvVars() {
} }
} }
/**
* Implements the 'pkgExec' command, used as a way to provide a Node.js
* interpreter for child_process.spawn()-like operations when the CLI is
* executing as a standalone zip package (built-in Node interpreter) and
* the system may not have a separate Node.js installation. A present use
* case is a patched version of the 'windosu' package that requires a
* Node.js interpreter to spawn a privileged child process.
*
* @param modFunc Path to a JS module that will be executed via require().
* The modFunc argument may optionally contain a function name separated
* by '::', for example '::main' in:
* 'C:\\snapshot\\balena-cli\\node_modules\\windosu\\lib\\pipe.js::main'
* in which case that function is executed in the require'd module.
* @param args Optional arguments to passed through process.argv and as
* arguments to the function specified via modFunc.
*/
export async function pkgExec(modFunc: string, args: string[]) {
const [modPath, funcName] = modFunc.split('::');
let replacedModPath = modPath;
const match = modPath
.replace(/\\/g, '/')
.match(/\/snapshot\/balena-cli\/(.+)/);
if (match) {
replacedModPath = `../${match[1]}`;
}
process.argv = [process.argv[0], process.argv[1], ...args];
try {
const mod: any = await import(replacedModPath);
if (funcName) {
await mod[funcName](...args);
}
} catch (err) {
console.error(`Error executing pkgExec "${modFunc}" [${args.join()}]`);
console.error(err);
}
}
export interface CachedUsername { export interface CachedUsername {
token: string; token: string;
username: string; username: string;

View File

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

View File

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

View File

@ -966,7 +966,7 @@ export async function makeBuildTasks(
deviceInfo: DeviceInfo, deviceInfo: DeviceInfo,
logger: Logger, logger: Logger,
projectName: string, projectName: string,
releaseHash: string = 'unavailable', releaseHash = 'unavailable',
preprocessHook?: (dockerfile: string) => string, preprocessHook?: (dockerfile: string) => string,
): Promise<MultiBuild.BuildTask[]> { ): Promise<MultiBuild.BuildTask[]> {
const multiBuild = await import('@balena/compose/dist/multibuild'); const multiBuild = await import('@balena/compose/dist/multibuild');
@ -1322,6 +1322,9 @@ async function pushAndUpdateServiceImages(
} }
} }
// Error messages are limited to 300KB characters in the API, so we truncate longer ones.
const MAX_ERROR_MESSAGE_LENGTH = 300_000;
async function pushServiceImages( async function pushServiceImages(
docker: Dockerode, docker: Dockerode,
logger: Logger, logger: Logger,
@ -1344,23 +1347,34 @@ async function pushServiceImages(
delete serviceImage.build_log; delete serviceImage.build_log;
} }
await releaseMod.updateImage( // These are the only update-able image fields in bC atm, and passing
pineClient, // the whole image object in v7+ would result the allowlist to reject the request.
serviceImage.id, const imagePayload = _.pick(serviceImage, [
// These are the only update-able image fields in bC atm, and passing 'end_timestamp',
// the whole image object in v7+ would result the allowlist to reject the request. 'project_type',
_.pick(serviceImage, [ 'error_message',
'end_timestamp', 'build_log',
'project_type', 'push_timestamp',
'error_message', 'status',
'build_log', 'content_hash',
'push_timestamp', 'dockerfile',
'status', 'image_size',
'content_hash', ]);
'dockerfile',
'image_size', if (
]), typeof imagePayload.error_message === 'string' &&
); imagePayload.error_message.length > MAX_ERROR_MESSAGE_LENGTH
) {
logger.logDebug(
`Truncating error message of image ${serviceImage.is_stored_at__image_location} to ${MAX_ERROR_MESSAGE_LENGTH} characters.`,
);
imagePayload.error_message = imagePayload.error_message.substring(
0,
MAX_ERROR_MESSAGE_LENGTH,
);
}
await releaseMod.updateImage(pineClient, serviceImage.id, imagePayload);
}, },
); );
} }
@ -1375,6 +1389,7 @@ export async function deployProject(
skipLogUpload: boolean, skipLogUpload: boolean,
projectPath: string, projectPath: string,
isDraft: boolean, isDraft: boolean,
imgDescriptors: ImageDescriptor[],
): Promise<import('@balena/compose/dist/release/models').ReleaseModel> { ): Promise<import('@balena/compose/dist/release/models').ReleaseModel> {
const releaseMod = await import('@balena/compose/dist/release'); const releaseMod = await import('@balena/compose/dist/release');
const { createRelease, tagServiceImages } = await import('./compose'); const { createRelease, tagServiceImages } = await import('./compose');
@ -1405,6 +1420,7 @@ export async function deployProject(
isDraft, isDraft,
contract?.version, contract?.version,
contract, contract,
imgDescriptors,
), ),
); );
const { client: pineClient, release, serviceImages } = $release; const { client: pineClient, release, serviceImages } = $release;
@ -1492,7 +1508,7 @@ export function createRunLoop(tick: (...args: any[]) => void) {
}, },
end() { end() {
clearInterval(timerId); clearInterval(timerId);
return runloop.onEnd(); runloop.onEnd();
}, },
}; };
return runloop; return runloop;
@ -1549,7 +1565,7 @@ function dropEmptyLinesStream() {
if (str.trim()) { if (str.trim()) {
this.push(str); this.push(str);
} }
return cb(); cb();
}); });
} }
@ -1570,7 +1586,7 @@ function buildLogCapture(objectMode: boolean, buffer: string[]) {
buffer.push(data); buffer.push(data);
} }
return cb(null, data); cb(null, data);
}); });
} }
@ -1585,13 +1601,17 @@ function buildProgressAdapter(inline: boolean) {
return through({ objectMode: true }, function (str, _enc, cb) { return through({ objectMode: true }, function (str, _enc, cb) {
if (str == null) { if (str == null) {
return cb(null, str); cb(null, str);
return;
} }
if (inline) { if (inline) {
return cb(null, { status: str }); cb(null, { status: str });
return;
} }
// We want to keep the regex match instead of startsWith as it also works with buffers
// eslint-disable-next-line @typescript-eslint/prefer-string-starts-ends-with
if (!/^Successfully tagged /.test(str)) { if (!/^Successfully tagged /.test(str)) {
const match = stepRegex.exec(str); const match = stepRegex.exec(str);
if (match) { if (match) {
@ -1607,7 +1627,7 @@ function buildProgressAdapter(inline: boolean) {
} }
} }
return cb(null, { status: str, progress }); cb(null, { status: str, progress });
}); });
} }

View File

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

View File

@ -19,7 +19,7 @@ import { getVisuals } from './lazy';
import { promisify } from 'util'; import { promisify } from 'util';
import type * as Dockerode from 'dockerode'; import type * as Dockerode from 'dockerode';
import type Logger = require('./logger'); import type Logger = require('./logger');
import type { Request } from 'request'; import type got from 'got';
const getBuilderPushEndpoint = function ( const getBuilderPushEndpoint = function (
baseUrl: string, baseUrl: string,
@ -75,7 +75,10 @@ const showPushProgress = function (message: string) {
return progressBar; return progressBar;
}; };
const uploadToPromise = (uploadRequest: Request, logger: Logger) => const uploadToPromise = (
uploadRequest: ReturnType<typeof got.stream.post>,
logger: Logger,
) =>
new Promise<{ buildId: number }>(function (resolve, reject) { new Promise<{ buildId: number }>(function (resolve, reject) {
uploadRequest.on('error', reject).on('data', function handleMessage(data) { uploadRequest.on('error', reject).on('data', function handleMessage(data) {
let obj; let obj;
@ -86,7 +89,7 @@ const uploadToPromise = (uploadRequest: Request, logger: Logger) =>
obj = JSON.parse(data); obj = JSON.parse(data);
} catch (e) { } catch (e) {
logger.logError('Error parsing reply from remote side'); logger.logError('Error parsing reply from remote side');
reject(e); reject(e as Error);
return; return;
} }
@ -106,10 +109,7 @@ const uploadToPromise = (uploadRequest: Request, logger: Logger) =>
}); });
}); });
/** const uploadImage = async function (
* @returns {Promise<{ buildId: number }>}
*/
const uploadImage = function (
imageStream: NodeJS.ReadableStream & { length: number }, imageStream: NodeJS.ReadableStream & { length: number },
token: string, token: string,
username: string, username: string,
@ -117,10 +117,9 @@ const uploadImage = function (
appName: string, appName: string,
logger: Logger, logger: Logger,
): Promise<{ buildId: number }> { ): Promise<{ buildId: number }> {
const request = require('request') as typeof import('request'); const { default: got } = await import('got');
const progressStream = const progressStream = await import('progress-stream');
require('progress-stream') as typeof import('progress-stream'); const zlib = await import('zlib');
const zlib = require('zlib') as typeof import('zlib');
// Need to strip off the newline // Need to strip off the newline
const progressMessage = logger const progressMessage = logger
@ -141,25 +140,26 @@ const uploadImage = function (
), ),
); );
const uploadRequest = request.post({ const uploadRequest = got.stream.post(
url: getBuilderPushEndpoint(url, username, appName), getBuilderPushEndpoint(url, username, appName),
headers: { {
'Content-Encoding': 'gzip', headers: {
'Content-Encoding': 'gzip',
Authorization: `Bearer ${token}`,
},
body: streamWithProgress.pipe(
zlib.createGzip({
level: 6,
}),
),
throwHttpErrors: false,
}, },
auth: { );
bearer: token,
},
body: streamWithProgress.pipe(
zlib.createGzip({
level: 6,
}),
),
});
return uploadToPromise(uploadRequest, logger); return uploadToPromise(uploadRequest, logger);
}; };
const uploadLogs = function ( const uploadLogs = async function (
logs: string, logs: string,
token: string, token: string,
url: string, url: string,
@ -167,14 +167,14 @@ const uploadLogs = function (
username: string, username: string,
appName: string, appName: string,
) { ) {
const request = require('request') as typeof import('request'); const { default: got } = await import('got');
return request.post({ return got.post(getBuilderLogPushEndpoint(url, buildId, username, appName), {
json: true, headers: {
url: getBuilderLogPushEndpoint(url, buildId, username, appName), Authorization: `Bearer ${token}`,
auth: {
bearer: token,
}, },
body: Buffer.from(logs), body: Buffer.from(logs),
responseType: 'json',
throwHttpErrors: false,
}); });
}; };

View File

@ -15,12 +15,12 @@
* limitations under the License. * limitations under the License.
*/ */
import * as _ from 'lodash'; import * as _ from 'lodash';
import * as request from 'request';
import type * as Stream from 'stream';
import { retry } from '../helpers'; import { retry } from '../helpers';
import Logger = require('../logger'); import Logger = require('../logger');
import * as ApiErrors from './errors'; import * as ApiErrors from './errors';
import { getBalenaSdk } from '../lazy';
import type { BalenaSDK } from 'balena-sdk';
export interface DeviceResponse { export interface DeviceResponse {
[key: string]: any; [key: string]: any;
@ -74,15 +74,15 @@ export class DeviceAPI {
public constructor( public constructor(
private logger: Logger, private logger: Logger,
addr: string, addr: string,
port: number = 48484, port = 48484,
) { ) {
this.deviceAddress = `http://${addr}:${port}/`; this.deviceAddress = `http://${addr}:${port}/`;
} }
// Either return nothing, or throw an error with the info // Either return nothing, or throw an error with the info
public async setTargetState(state: any): Promise<void> { public async setTargetState(state: Record<string, any>) {
const url = this.getUrlForAction('setTargetState'); const url = this.getUrlForAction('setTargetState');
return DeviceAPI.promisifiedRequest( await DeviceAPI.sendRequest(
{ {
method: 'POST', method: 'POST',
url, url,
@ -96,37 +96,37 @@ export class DeviceAPI {
public async getTargetState() { public async getTargetState() {
const url = this.getUrlForAction('getTargetState'); const url = this.getUrlForAction('getTargetState');
return DeviceAPI.promisifiedRequest( return await DeviceAPI.sendRequest(
{ {
method: 'GET', method: 'GET',
url, url,
json: true, json: true,
}, },
this.logger, this.logger,
).then((body) => { ).then(({ state }: { state: Record<string, any> }) => {
return body.state; return state;
}); });
} }
public async getDeviceInformation(): Promise<DeviceInfo> { public async getDeviceInformation() {
const url = this.getUrlForAction('getDeviceInformation'); const url = this.getUrlForAction('getDeviceInformation');
return DeviceAPI.promisifiedRequest( return await DeviceAPI.sendRequest(
{ {
method: 'GET', method: 'GET',
url, url,
json: true, json: true,
}, },
this.logger, this.logger,
).then((body) => { ).then(({ info }: { info: DeviceInfo }) => {
return body.info; return info;
}); });
} }
public async getContainerId(serviceName: string): Promise<string> { public async getContainerId(serviceName: string): Promise<string> {
const url = this.getUrlForAction('containerId'); const url = this.getUrlForAction('containerId');
const body = await DeviceAPI.promisifiedRequest( const body = await DeviceAPI.sendRequest(
{ {
method: 'GET', method: 'GET',
url, url,
@ -146,10 +146,10 @@ export class DeviceAPI {
return body.containerId; return body.containerId;
} }
public async ping(): Promise<void> { public async ping() {
const url = this.getUrlForAction('ping'); const url = this.getUrlForAction('ping');
return DeviceAPI.promisifiedRequest( await DeviceAPI.sendRequest(
{ {
method: 'GET', method: 'GET',
url, url,
@ -158,10 +158,10 @@ export class DeviceAPI {
); );
} }
public getVersion(): Promise<string> { public async getVersion(): Promise<string> {
const url = this.getUrlForAction('version'); const url = this.getUrlForAction('version');
return DeviceAPI.promisifiedRequest({ return await DeviceAPI.sendRequest({
method: 'GET', method: 'GET',
url, url,
json: true, json: true,
@ -176,10 +176,10 @@ export class DeviceAPI {
}); });
} }
public getStatus(): Promise<Status> { public async getStatus() {
const url = this.getUrlForAction('status'); const url = this.getUrlForAction('status');
return DeviceAPI.promisifiedRequest({ return await DeviceAPI.sendRequest({
method: 'GET', method: 'GET',
url, url,
json: true, json: true,
@ -194,91 +194,60 @@ export class DeviceAPI {
}); });
} }
public getLogStream(): Promise<Stream.Readable> { public async getLogStream() {
const url = this.getUrlForAction('logs'); const url = this.getUrlForAction('logs');
const sdk = getBalenaSdk();
// Don't use the promisified version here as we want to stream the output const stream = await sdk.request.stream({ url });
return new Promise((resolve, reject) => { stream.on('response', (res) => {
const req = request.get(url); if (res.statusCode !== 200) {
throw new ApiErrors.DeviceAPIError(
req.on('error', reject).on('response', async (res) => { 'Non-200 response from log streaming endpoint',
if (res.statusCode !== 200) { );
reject( }
new ApiErrors.DeviceAPIError( res.socket.setKeepAlive(true, 1000);
'Non-200 response from log streaming endpoint',
),
);
return;
}
try {
res.socket.setKeepAlive(true, 1000);
} catch (error) {
reject(error);
}
resolve(res);
});
}); });
return stream;
} }
private getUrlForAction(action: keyof typeof deviceEndpoints): string { private getUrlForAction(action: keyof typeof deviceEndpoints) {
return `${this.deviceAddress}${deviceEndpoints[action]}`; return `${this.deviceAddress}${deviceEndpoints[action]}`;
} }
// A helper method for promisifying general (non-streaming) requests. Streaming // A helper method for promisifying general (non-streaming) requests. Streaming
// requests should use a seperate setup // requests should use a seperate setup
private static async promisifiedRequest< private static async sendRequest(
T extends Parameters<typeof request>[0], opts: Parameters<BalenaSDK['request']['send']>[number],
>(opts: T, logger?: Logger): Promise<any> { logger?: Logger,
interface ObjectWithUrl { ) {
url?: string; if (logger != null && opts.url != null) {
} logger.logDebug(`Sending request to ${opts.url}`);
if (logger != null) {
let url: string | null = null;
if (_.isObject(opts) && (opts as ObjectWithUrl).url != null) {
// the `as string` shouldn't be necessary, but the type system
// is getting a little confused
url = (opts as ObjectWithUrl).url as string;
} else if (typeof opts === 'string') {
url = opts;
}
if (url != null) {
logger.logDebug(`Sending request to ${url}`);
}
} }
const sdk = getBalenaSdk();
const doRequest = async () => { const doRequest = async () => {
return await new Promise((resolve, reject) => { const response = await sdk.request.send(opts);
return request(opts, (err, response, body) => { const bodyError =
if (err) { typeof response.body === 'string'
return reject(err); ? response.body
} : response.body.message;
switch (response.statusCode) { switch (response.statusCode) {
case 200: case 200:
return resolve(body); return response.body;
case 400: case 400:
return reject( throw new ApiErrors.BadRequestDeviceAPIError(bodyError);
new ApiErrors.BadRequestDeviceAPIError(body.message), case 503:
); throw new ApiErrors.ServiceUnavailableAPIError(bodyError);
case 503: default:
return reject( new ApiErrors.DeviceAPIError(bodyError);
new ApiErrors.ServiceUnavailableAPIError(body.message), }
);
default:
return reject(new ApiErrors.DeviceAPIError(body.message));
}
});
});
}; };
return await retry({ return await retry({
func: doRequest, func: doRequest,
initialDelayMs: 2000, initialDelayMs: 2000,
maxAttempts: 6, maxAttempts: 6,
label: `Supervisor API (${opts.method} ${(opts as any).url})`, label: `Supervisor API (${opts.method} ${opts.url})`,
}); });
} }
} }
export default DeviceAPI;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -159,9 +159,9 @@ especially discouraged in scripts (e.g. CI environments).`;
export const devModeInfo = `\ export const devModeInfo = `\
The '--dev' option is used to configure balenaOS to operate in development mode, The '--dev' option is used to configure balenaOS to operate in development mode,
allowing anauthenticated root ssh access and exposing network ports such as allowing unauthenticated root ssh access and exposing network ports such as
balenaEngine's 2375 (unencrypted). This option causes \`"developmentMode": true\` balenaEngine's 2375 (unencrypted). This option causes \`"developmentMode": true\`
to be inserted in the 'config.json' file in the image's boot partion. Development to be inserted in the 'config.json' file in the image's boot partition. Development
mode (as a configurable option) is applicable to balenaOS releases from early mode (as a configurable option) is applicable to balenaOS releases from early
2022. Older releases have separate development and production balenaOS images 2022. Older releases have separate development and production balenaOS images
that cannot be reconfigured through 'config.json' or the '--dev' option. Do not that cannot be reconfigured through 'config.json' or the '--dev' option. Do not

View File

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

View File

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

View File

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

View File

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

View File

@ -50,7 +50,7 @@ export function copyQemu(context: string, arch: string) {
.then(() => getQemuPath(arch)) .then(() => getQemuPath(arch))
.then( .then(
(qemu) => (qemu) =>
new Promise(function (resolve, reject) { new Promise<void>(function (resolve, reject) {
const read = fs.createReadStream(qemu); const read = fs.createReadStream(qemu);
const write = fs.createWriteStream(binPath); const write = fs.createWriteStream(binPath);
@ -94,7 +94,7 @@ async function installQemu(arch: string, qemuPath: string) {
const urlVersion = encodeURIComponent(QEMU_VERSION); const urlVersion = encodeURIComponent(QEMU_VERSION);
const qemuUrl = `https://github.com/balena-io/qemu/releases/download/${urlVersion}/${urlFile}`; const qemuUrl = `https://github.com/balena-io/qemu/releases/download/${urlVersion}/${urlFile}`;
const request = await import('request'); const { default: got } = await import('got');
const fs = await import('fs'); const fs = await import('fs');
const zlib = await import('zlib'); const zlib = await import('zlib');
const tar = await import('tar-stream'); const tar = await import('tar-stream');
@ -114,10 +114,11 @@ async function installQemu(arch: string, qemuPath: string) {
stream.resume(); stream.resume();
} }
} catch (err) { } catch (err) {
reject(err); reject(err as Error);
} }
}); });
request(qemuUrl) got.stream
.get(qemuUrl)
.on('error', reject) .on('error', reject)
.pipe(zlib.createGunzip()) .pipe(zlib.createGunzip())
.on('error', reject) .on('error', reject)

View File

@ -16,7 +16,8 @@ limitations under the License.
import type { BalenaSDK } from 'balena-sdk'; import type { BalenaSDK } from 'balena-sdk';
import * as JSONStream from 'JSONStream'; import * as JSONStream from 'JSONStream';
import * as readline from 'readline'; import * as readline from 'readline';
import * as request from 'request'; import type { PlainResponse } from 'got';
import type got from 'got';
import type { RegistrySecrets } from '@balena/compose/dist/multibuild'; import type { RegistrySecrets } from '@balena/compose/dist/multibuild';
import type * as Stream from 'stream'; import type * as Stream from 'stream';
import streamToPromise = require('stream-to-promise'); import streamToPromise = require('stream-to-promise');
@ -110,7 +111,7 @@ export async function startRemoteBuild(
const [buildRequest, stream] = await getRemoteBuildStream(build); const [buildRequest, stream] = await getRemoteBuildStream(build);
// Setup CTRL-C handler so the user can interrupt the build // Setup CTRL-C handler so the user can interrupt the build
let cancellationPromise = Promise.resolve(); let cancellationPromise: Promise<void> | undefined;
const sigintHandler = () => { const sigintHandler = () => {
process.exitCode = 130; process.exitCode = 130;
console.error('\nReceived SIGINT, cleaning up. Please wait.'); console.error('\nReceived SIGINT, cleaning up. Please wait.');
@ -119,7 +120,7 @@ export async function startRemoteBuild(
} catch (err) { } catch (err) {
console.error(err.message); console.error(err.message);
} finally { } finally {
buildRequest.abort(); buildRequest.destroy();
const sigintErr = new SIGINTError('Build aborted on SIGINT signal'); const sigintErr = new SIGINTError('Build aborted on SIGINT signal');
sigintErr.code = 'SIGINT'; sigintErr.code = 'SIGINT';
stream.emit('error', sigintErr); stream.emit('error', sigintErr);
@ -246,7 +247,8 @@ function getBuilderMessageHandler(
console.error(`[debug] handling message: ${JSON.stringify(obj)}`); console.error(`[debug] handling message: ${JSON.stringify(obj)}`);
} }
if (obj.type != null && obj.type === 'metadata') { if (obj.type != null && obj.type === 'metadata') {
return handleBuilderMetadata(obj, build); handleBuilderMetadata(obj, build);
return;
} }
if (obj.message) { if (obj.message) {
readline.clearLine(process.stdout, 0); readline.clearLine(process.stdout, 0);
@ -336,32 +338,29 @@ async function getTarStream(build: RemoteBuild): Promise<Stream.Readable> {
/** /**
* Initiate a POST HTTP request to the remote builder and add some event * Initiate a POST HTTP request to the remote builder and add some event
* listeners. * listeners.
*
* ¡! Note: this function must be synchronous because of a bug in the `request`
* library that requires the following two steps to take place in the same
* iteration of Node's event loop: (1) adding a listener for the 'response'
* event and (2) calling request.pipe():
* https://github.com/request/request/issues/887
*/ */
function createRemoteBuildRequest( async function createRemoteBuildRequest(
build: RemoteBuild, build: RemoteBuild,
tarStream: Stream.Readable, tarStream: Stream.Readable,
builderUrl: string, builderUrl: string,
onError: (error: Error) => void, onError: (error: Error) => void,
): request.Request { ) {
const zlib = require('zlib') as typeof import('zlib'); const { default: got } = await import('got');
const zlib = await import('zlib');
if (DEBUG_MODE) { if (DEBUG_MODE) {
console.error(`[debug] Connecting to builder at ${builderUrl}`); console.error(`[debug] Connecting to builder at ${builderUrl}`);
} }
return request return got.stream
.post({ .post(builderUrl, {
url: builderUrl, headers: {
auth: { bearer: build.auth }, Authorization: `Bearer ${build.auth}`,
headers: { 'Content-Encoding': 'gzip' }, 'Content-Encoding': 'gzip',
},
body: tarStream.pipe(zlib.createGzip({ level: 6 })), body: tarStream.pipe(zlib.createGzip({ level: 6 })),
throwHttpErrors: false,
}) })
.once('error', onError) // `.once` because the handler re-emits .once('error', onError) // `.once` because the handler re-emits
.once('response', (response: request.RequestResponse) => { .once('response', (response: PlainResponse) => {
if (response.statusCode >= 100 && response.statusCode < 400) { if (response.statusCode >= 100 && response.statusCode < 400) {
if (DEBUG_MODE) { if (DEBUG_MODE) {
console.error( console.error(
@ -373,8 +372,8 @@ function createRemoteBuildRequest(
'Remote builder responded with HTTP error:', 'Remote builder responded with HTTP error:',
`${response.statusCode} ${response.statusMessage}`, `${response.statusCode} ${response.statusMessage}`,
]; ];
if (response.body) { if (response.rawBody) {
msgArr.push(response.body); msgArr.push(response.rawBody.toString());
} }
onError(new ExpectedError(msgArr.join('\n'))); onError(new ExpectedError(msgArr.join('\n')));
} }
@ -383,7 +382,7 @@ function createRemoteBuildRequest(
async function getRemoteBuildStream( async function getRemoteBuildStream(
build: RemoteBuild, build: RemoteBuild,
): Promise<[request.Request, Stream.Stream]> { ): Promise<[ReturnType<typeof got.stream.post>, Stream.Stream]> {
const builderUrl = await getBuilderEndpoint( const builderUrl = await getBuilderEndpoint(
build.baseUrl, build.baseUrl,
build.appSlug, build.appSlug,
@ -411,7 +410,7 @@ async function getRemoteBuildStream(
} }
const tarStream = await getTarStream(build); const tarStream = await getTarStream(build);
const buildRequest = createRemoteBuildRequest( const buildRequest = await createRemoteBuildRequest(
build, build,
tarStream, tarStream,
builderUrl, builderUrl,
@ -423,10 +422,20 @@ async function getRemoteBuildStream(
stream = buildRequest.pipe(JSONStream.parse('*')) as NodeJS.ReadStream; stream = buildRequest.pipe(JSONStream.parse('*')) as NodeJS.ReadStream;
} }
stream = stream stream = stream
.once('error', () => uploadSpinner.stop()) .once('error', () => {
.once('close', () => uploadSpinner.stop()) uploadSpinner.stop();
.once('data', () => uploadSpinner.stop()) })
.once('end', () => uploadSpinner.stop()) .once('close', () => {
.once('finish', () => uploadSpinner.stop()); uploadSpinner.stop();
})
.once('data', () => {
uploadSpinner.stop();
})
.once('end', () => {
uploadSpinner.stop();
})
.once('finish', () => {
uploadSpinner.stop();
});
return [buildRequest, stream]; return [buildRequest, stream];
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -20,7 +20,7 @@ import * as chaiAsPromised from 'chai-as-promised';
import * as ejs from 'ejs'; import * as ejs from 'ejs';
import * as fs from 'fs'; import * as fs from 'fs';
import * as path from 'path'; import * as path from 'path';
import * as request from 'request'; import got from 'got';
import * as sinon from 'sinon'; import * as sinon from 'sinon';
import { LoginServer } from '../../build/auth/server'; import { LoginServer } from '../../build/auth/server';
@ -31,7 +31,7 @@ chai.use(chaiAsPromised);
const { expect } = chai; const { expect } = chai;
async function getPage(name: string): Promise<string> { function getPage(name: string): string {
const pagePath = path.join( const pagePath = path.join(
__dirname, __dirname,
'..', '..',
@ -61,38 +61,30 @@ describe('Login server:', function () {
server.shutdown(); server.shutdown();
}); });
async function testLogin(opt: { async function testLogin({
verb = 'post',
...opt
}: {
expectedBody: string; expectedBody: string;
expectedErrorMsg?: string; expectedErrorMsg?: string;
expectedStatusCode: number; expectedStatusCode: number;
expectedToken: string; expectedToken: string;
urlPath?: string; urlPath?: string;
verb?: string; verb?: 'post' | 'put';
}) { }) {
opt.urlPath = opt.urlPath ?? addr.urlPath; opt.urlPath = opt.urlPath ?? addr.urlPath;
const post = opt.verb const res = await got[verb](
? ((request as any)[opt.verb] as typeof request.post) `http://${addr.host}:${addr.port}${opt.urlPath}`,
: request.post; {
await new Promise<void>((resolve, reject) => { form: {
post( token: opt.expectedToken,
`http://${addr.host}:${addr.port}${opt.urlPath}`,
{
form: {
token: opt.expectedToken,
},
}, },
function (error, response, body) { throwHttpErrors: false,
try { },
expect(error).to.not.exist; );
expect(response.statusCode).to.equal(opt.expectedStatusCode);
expect(body).to.equal(opt.expectedBody); expect(res.body).to.equal(opt.expectedBody);
resolve(); expect(res.statusCode).to.equal(opt.expectedStatusCode);
} catch (err) {
reject(err);
}
},
);
});
try { try {
const token = await server.awaitForToken(); const token = await server.awaitForToken();
@ -127,14 +119,14 @@ describe('Login server:', function () {
expectedStatusCode: 404, expectedStatusCode: 404,
expectedToken: tokens.johndoe.token, expectedToken: tokens.johndoe.token,
expectedErrorMsg: 'Unknown path or verb', expectedErrorMsg: 'Unknown path or verb',
verb: 'get', verb: 'put',
}); });
}); });
describe('given the token authenticates with the server', function () { describe('given the token authenticates with the server', function () {
beforeEach(function () { beforeEach(function () {
this.loginIfTokenValidStub = sinon.stub(utils, 'loginIfTokenValid'); this.loginIfTokenValidStub = sinon.stub(utils, 'loginIfTokenValid');
this.loginIfTokenValidStub.returns(Promise.resolve(true)); this.loginIfTokenValidStub.resolves(true);
}); });
afterEach(function () { afterEach(function () {
@ -143,7 +135,7 @@ describe('Login server:', function () {
it('should eventually be the token', async () => { it('should eventually be the token', async () => {
await testLogin({ await testLogin({
expectedBody: await getPage('success'), expectedBody: getPage('success'),
expectedStatusCode: 200, expectedStatusCode: 200,
expectedToken: tokens.johndoe.token, expectedToken: tokens.johndoe.token,
}); });
@ -153,7 +145,7 @@ describe('Login server:', function () {
describe('given the token does not authenticate with the server', function () { describe('given the token does not authenticate with the server', function () {
beforeEach(function () { beforeEach(function () {
this.loginIfTokenValidStub = sinon.stub(utils, 'loginIfTokenValid'); this.loginIfTokenValidStub = sinon.stub(utils, 'loginIfTokenValid');
return this.loginIfTokenValidStub.returns(Promise.resolve(false)); return this.loginIfTokenValidStub.resolves(false);
}); });
afterEach(function () { afterEach(function () {
@ -162,7 +154,7 @@ describe('Login server:', function () {
it('should be rejected', async () => { it('should be rejected', async () => {
await testLogin({ await testLogin({
expectedBody: await getPage('error'), expectedBody: getPage('error'),
expectedStatusCode: 401, expectedStatusCode: 401,
expectedToken: tokens.johndoe.token, expectedToken: tokens.johndoe.token,
expectedErrorMsg: 'Invalid token', expectedErrorMsg: 'Invalid token',
@ -171,7 +163,7 @@ describe('Login server:', function () {
it('should be rejected if no token', async () => { it('should be rejected if no token', async () => {
await testLogin({ await testLogin({
expectedBody: await getPage('error'), expectedBody: getPage('error'),
expectedStatusCode: 401, expectedStatusCode: 401,
expectedToken: '', expectedToken: '',
expectedErrorMsg: 'No token', expectedErrorMsg: 'No token',
@ -180,7 +172,7 @@ describe('Login server:', function () {
it('should be rejected if token is malformed', async () => { it('should be rejected if token is malformed', async () => {
await testLogin({ await testLogin({
expectedBody: await getPage('error'), expectedBody: getPage('error'),
expectedStatusCode: 401, expectedStatusCode: 401,
expectedToken: 'asdf', expectedToken: 'asdf',
expectedErrorMsg: 'Invalid token', expectedErrorMsg: 'Invalid token',

View File

@ -267,15 +267,15 @@ describe('balena build', function () {
...fsMod, ...fsMod,
promises: { promises: {
...fsMod.promises, ...fsMod.promises,
access: async (p: string) => access: (p: string) =>
p === qemuBinPath ? undefined : fsMod.promises.access(p), p === qemuBinPath ? undefined : fsMod.promises.access(p),
stat: async (p: string) => stat: (p: string) =>
p === qemuBinPath ? { size: 1 } : fsMod.promises.stat(p), p === qemuBinPath ? { size: 1 } : fsMod.promises.stat(p),
}, },
}); });
mock(qemuModPath, { mock(qemuModPath, {
...qemuMod, ...qemuMod,
copyQemu: async () => '', copyQemu: () => '',
}); });
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' });
@ -442,6 +442,93 @@ describe('balena build', function () {
}); });
}); });
it('should create the expected tar stream (docker-compose --nologs)', 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' },
},
service2: {
'Dockerfile-alt': { fileSize: 13, type: 'file' },
'file2-crlf.sh': {
fileSize: isWindows ? 12 : 14,
testStream: isWindows ? expectStreamNoCRLF : undefined,
type: 'file',
},
'src/file1.sh': { fileSize: 12, 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: {
COMPOSE_ARG: 'A',
barg: 'b',
SERVICE1_VAR: 'This is a service specific variable',
},
cachefrom: ['my/img1', 'my/img2'],
}),
),
service2: Object.entries(
_.merge({}, commonComposeQueryParamsIntel, {
buildargs: {
COMPOSE_ARG: 'A',
barg: 'b',
},
cachefrom: ['my/img1', 'my/img2'],
dockerfile: 'Dockerfile-alt',
}),
),
};
const expectedResponseLines: string[] = [
...commonResponseLines[responseFilename],
...getDockerignoreWarn1(
[path.join(projectPath, 'service2', '.dockerignore')],
'build',
),
];
if (isWindows) {
expectedResponseLines.push(
`[Info] Converting line endings CRLF -> LF for file: ${path.join(
projectPath,
'service2',
'file2-crlf.sh',
)}`,
);
}
docker.expectGetInfo({});
docker.expectGetManifestNucAlpine();
docker.expectGetManifestBusybox();
await testDockerBuildStream({
commandLine: `build ${projectPath} --deviceType nuc --arch amd64 -B COMPOSE_ARG=A -B barg=b --cache-from my/img1,my/img2 --nologs`,
dockerMock: docker,
expectedFilesByService,
expectedQueryParamsByService,
expectedResponseLines,
projectPath,
responseBody,
responseCode: 200,
services: ['service1', 'service2'],
});
});
it('should create the expected tar stream (docker-compose, --multi-dockerignore)', async () => { it('should create the expected tar stream (docker-compose, --multi-dockerignore)', async () => {
const projectPath = path.join(projectsPath, 'docker-compose', 'basic'); const projectPath = path.join(projectsPath, 'docker-compose', 'basic');
const service1Dockerfile = ( const service1Dockerfile = (

View File

@ -27,7 +27,7 @@ import * as sinon from 'sinon';
import { BalenaAPIMock } from '../nock/balena-api-mock'; import { BalenaAPIMock } from '../nock/balena-api-mock';
import { expectStreamNoCRLF, testDockerBuildStream } from '../docker-build'; import { expectStreamNoCRLF, testDockerBuildStream } from '../docker-build';
import { DockerMock, dockerResponsePath } from '../nock/docker-mock'; import { DockerMock, dockerResponsePath } from '../nock/docker-mock';
import { cleanOutput, runCommand, switchSentry } from '../helpers'; import { cleanOutput, runCommand } from '../helpers';
import type { import type {
ExpectedTarStreamFiles, ExpectedTarStreamFiles,
ExpectedTarStreamFilesByService, ExpectedTarStreamFilesByService,
@ -262,7 +262,6 @@ describe('balena deploy', function () {
}); });
it('should update a release with status="failed" on error (single container)', async () => { it('should update a release with status="failed" on error (single container)', async () => {
let sentryStatus: boolean | undefined;
const projectPath = path.join(projectsPath, 'no-docker-compose', 'basic'); const projectPath = path.join(projectsPath, 'no-docker-compose', 'basic');
const expectedFiles: ExpectedTarStreamFiles = { const expectedFiles: ExpectedTarStreamFiles = {
'src/.dockerignore': { fileSize: 16, type: 'file' }, 'src/.dockerignore': { fileSize: 16, type: 'file' },
@ -319,7 +318,6 @@ describe('balena deploy', function () {
api.expectPostImageLabel(); api.expectPostImageLabel();
try { try {
sentryStatus = await switchSentry(false);
sinon.stub(process, 'exit'); sinon.stub(process, 'exit');
await testDockerBuildStream({ await testDockerBuildStream({
@ -337,9 +335,8 @@ describe('balena deploy', function () {
}); });
expect(failedImagePatchRequests).to.equal(maxRequestRetries); expect(failedImagePatchRequests).to.equal(maxRequestRetries);
} finally { } finally {
await switchSentry(sentryStatus); // We mock process.exit and need to force cast it to a SinonStub to restore it
// @ts-expect-error claims restore does not exist (process.exit as unknown as sinon.SinonStub).restore();
process.exit.restore();
} }
}); });

View File

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

View File

@ -21,8 +21,6 @@ import { BalenaAPIMock } from '../../nock/balena-api-mock';
import { cleanOutput, runCommand } from '../../helpers'; import { cleanOutput, runCommand } from '../../helpers';
import { SupervisorMock } from '../../nock/supervisor-mock'; import { SupervisorMock } from '../../nock/supervisor-mock';
const itS = process.env.BALENA_CLI_TEST_TYPE === 'standalone' ? it : it.skip;
describe('balena device logs', function () { describe('balena device logs', function () {
let api: BalenaAPIMock; let api: BalenaAPIMock;
let supervisor: SupervisorMock; let supervisor: SupervisorMock;
@ -39,10 +37,7 @@ describe('balena device logs', function () {
supervisor.done(); supervisor.done();
}); });
// skip non-standalone tests because nock's mock socket causes the error: it('should reach the expected endpoints on a local device', async () => {
// "setKeepAliveInterval expects an instance of socket as its first argument"
// in utils/device/api.ts: NetKeepalive.setKeepAliveInterval(sock, 5000);
itS('should reach the expected endpoints on a local device', async () => {
supervisor.expectGetPing(); supervisor.expectGetPing();
supervisor.expectGetLogs(); supervisor.expectGetLogs();
supervisor.expectGetLogs(); supervisor.expectGetLogs();

Some files were not shown because too many files have changed in this diff Show More