mirror of
https://github.com/balena-io/balena-cli.git
synced 2025-06-24 18:45:07 +00:00
Compare commits
90 Commits
v12.45.0
...
update-res
Author | SHA1 | Date | |
---|---|---|---|
9b6fb62e4f | |||
cc60e86507 | |||
bd774e8553 | |||
c493c33e38 | |||
9487b33144 | |||
befdae1b90 | |||
08dfc945f3 | |||
8791c2f4e1 | |||
be306e6a20 | |||
6cfff72c59 | |||
adae718c2e | |||
132e1a63b2 | |||
a18e182ae4 | |||
e098cdca17 | |||
b42af74983 | |||
8bb211e441 | |||
ffccbfba12 | |||
56c1af50c0 | |||
8b9e3ccdc8 | |||
de95262f93 | |||
ed49938504 | |||
52ad0f6a57 | |||
7f6738c73c | |||
88fc3f7714 | |||
1afb29b923 | |||
09a4e8db2d | |||
6c81440428 | |||
3eca65ce0d | |||
6319b9dc13 | |||
290acaecbb | |||
305c9045f0 | |||
b701151769 | |||
e03bbb7275 | |||
3fd66c39ae | |||
b30075a18b | |||
a4fc95e99b | |||
63d8e5e6a3 | |||
6244af3464 | |||
8773927b3f | |||
29a3fd40a2 | |||
d6faf060e6 | |||
352fd197b7 | |||
afb6f938b7 | |||
d3adbcdba9 | |||
33fce1f24f | |||
ab90a5f150 | |||
a8b2212fed | |||
6bb8df30dd | |||
0327ed766d | |||
1009958340 | |||
5ce17ea70f | |||
9c821511b1 | |||
d793335287 | |||
dc59b7e4b0 | |||
370b844538 | |||
a8c2724929 | |||
09dd2dd354 | |||
f3ab41841a | |||
3dee30a0fe | |||
d34073f695 | |||
24fe6666e4 | |||
3fd5981085 | |||
08ee8643cb | |||
8db36ccec9 | |||
deb3e4c4ac | |||
a8ff21af69 | |||
4c54d6c171 | |||
83f213c007 | |||
d0cdc900a2 | |||
9937b91606 | |||
972c2470c5 | |||
7d568a928b | |||
2331e0a3e5 | |||
cb9b6be24b | |||
c2d3eee7cc | |||
d8b08f7272 | |||
819bdac354 | |||
318de8f017 | |||
2b0341e12a | |||
21f7463607 | |||
19fd3094d1 | |||
7c4974f4f5 | |||
3b56ed278e | |||
254ef1c8cf | |||
d11f49e0f8 | |||
48d7d0ef5e | |||
c7bbbc4159 | |||
d2fabcaf30 | |||
e137c2aed2 | |||
58704b08d3 |
File diff suppressed because it is too large
Load Diff
276
CHANGELOG.md
276
CHANGELOG.md
@ -4,6 +4,282 @@ All notable changes to this project will be documented in this file
|
||||
automatically by Versionist. DO NOT EDIT THIS FILE MANUALLY!
|
||||
This project adheres to [Semantic Versioning](http://semver.org/).
|
||||
|
||||
## 12.50.2 - 2021-10-05
|
||||
|
||||
* Error message when renaming a fleet now mentions the target name. [Carlo Miguel F. Cruz]
|
||||
|
||||
## 12.50.1 - 2021-09-30
|
||||
|
||||
* Update dependencies (@sentry/node error reporting) [Paulo Castro]
|
||||
* Replace mixpanel dependency with simple GET request [Paulo Castro]
|
||||
* Avoid NockMock warnings during standalone executable testing [Paulo Castro]
|
||||
* Fix help output for 'release finalize' command [Paulo Castro]
|
||||
|
||||
## 12.50.0 - 2021-09-28
|
||||
|
||||
* Add support for releases [Paul Jonathan Zoulin]
|
||||
|
||||
## 12.49.0 - 2021-09-23
|
||||
|
||||
* build, deploy: Improve logging of image build messages [Paulo Castro]
|
||||
* build, deploy: Add support for multiarch base images [toochevere]
|
||||
|
||||
## 12.48.15 - 2021-09-22
|
||||
|
||||
|
||||
<details>
|
||||
<summary> Update balena-sdk to 15.51.1 [Nitish Agarwal] </summary>
|
||||
|
||||
> ### balena-sdk-15.51.1 - 2021-09-20
|
||||
>
|
||||
>
|
||||
> <details>
|
||||
> <summary> Update balena-request to v11.4.2 [Kyle Harding] </summary>
|
||||
>
|
||||
>> #### balena-request-11.4.2 - 2021-09-20
|
||||
>>
|
||||
>> * Allow overriding the default zlib flush setting [Kyle Harding]
|
||||
>>
|
||||
> </details>
|
||||
>
|
||||
>
|
||||
> ### balena-sdk-15.51.0 - 2021-09-16
|
||||
>
|
||||
> * os.getConfig: Add typings for the provisioningKeyName option [Nitish Agarwal]
|
||||
>
|
||||
> ### balena-sdk-15.50.1 - 2021-09-13
|
||||
>
|
||||
> * models/os: Always first normalize the device type slug [Thodoris Greasidis]
|
||||
>
|
||||
> ### balena-sdk-15.50.0 - 2021-09-10
|
||||
>
|
||||
> * Add release.finalize to promote draft releases to final [toochevere]
|
||||
>
|
||||
> ### balena-sdk-15.49.1 - 2021-09-10
|
||||
>
|
||||
> * typings: Drop the v5-model-only application_type.is_host_os [Thodoris Greasidis]
|
||||
>
|
||||
> ### balena-sdk-15.49.0 - 2021-09-06
|
||||
>
|
||||
> * os.getSupportedOsUpdateVersions: Use the hostApp releases [Thodoris Greasidis]
|
||||
> * os.download: Use the hostApp for finding the latest release [Thodoris Greasidis]
|
||||
>
|
||||
> ### balena-sdk-15.48.3 - 2021-08-27
|
||||
>
|
||||
>
|
||||
> <details>
|
||||
> <summary> Update balena-request to 11.4.1 [Kyle Harding] </summary>
|
||||
>
|
||||
>> #### balena-request-11.4.1 - 2021-08-27
|
||||
>>
|
||||
>> * Allow more lenient gzip decompression [Kyle Harding]
|
||||
>>
|
||||
> </details>
|
||||
>
|
||||
>
|
||||
> ### balena-sdk-15.48.2 - 2021-08-27
|
||||
>
|
||||
> * Improve hostapp.getAllOsVersions performance & reduce fetched data [Thodoris Greasidis]
|
||||
>
|
||||
> ### balena-sdk-15.48.1 - 2021-08-27
|
||||
>
|
||||
> * Update typescript to 4.4.2 [Thodoris Greasidis]
|
||||
>
|
||||
</details>
|
||||
|
||||
## 12.48.14 - 2021-09-20
|
||||
|
||||
* os download: Avoid incomplete os downloads appearing as successful [Kyle Harding]
|
||||
|
||||
## 12.48.13 - 2021-09-17
|
||||
|
||||
* config inject: Remove requirement of being logged in [Paulo Castro]
|
||||
|
||||
## 12.48.12 - 2021-09-10
|
||||
|
||||
* build/deploy: Update QEMU to 6.0.0 for emulated builds [Kyle Harding]
|
||||
|
||||
## 12.48.11 - 2021-09-10
|
||||
|
||||
* build, deploy: Fix processing of '--tag' option [Paulo Castro]
|
||||
|
||||
## 12.48.10 - 2021-09-10
|
||||
|
||||
* push: Await and retry supervisor API requests to a local device [Paulo Castro]
|
||||
|
||||
## 12.48.9 - 2021-09-09
|
||||
|
||||
* chore: Update net-keepalive dependency (fix CLI packaging errors) [Paulo Castro]
|
||||
|
||||
## 12.48.8 - 2021-09-08
|
||||
|
||||
* v13 preparations: Add feature switch for removal of '--gitignore' (push, build) [Paulo Castro]
|
||||
* v13 preparations: Adjust test cases for 'balena envs' [Paulo Castro]
|
||||
* v13 preparations: Adjust test cases for 'balena devices' [Paulo Castro]
|
||||
|
||||
## 12.48.7 - 2021-09-07
|
||||
|
||||
* device move: Improve types & reduce the number of API requests [Thodoris Greasidis]
|
||||
* device move: Rely on the device type model to get the compatible apps [Thodoris Greasidis]
|
||||
|
||||
## 12.48.6 - 2021-09-07
|
||||
|
||||
* preload: Rely on the device type model to get the compatible apps [Thodoris Greasidis]
|
||||
|
||||
## 12.48.5 - 2021-09-07
|
||||
|
||||
* preload: Replace my_application query with the SDKs application.getAll() [Thodoris Greasidis]
|
||||
|
||||
## 12.48.4 - 2021-08-31
|
||||
|
||||
* os download: Use the hostApps instead of the device-types/v1 endpoint [Thodoris Greasidis]
|
||||
|
||||
## 12.48.3 - 2021-08-31
|
||||
|
||||
* balena deploy: Retrieve the cpu arch as part of the device type resource [Thodoris Greasidis]
|
||||
|
||||
## 12.48.2 - 2021-08-30
|
||||
|
||||
* Clarify installation instructions [Paulo Castro]
|
||||
|
||||
## 12.48.1 - 2021-08-27
|
||||
|
||||
* Improve error handling (remove most occurrences of process.exit()) [Paulo Castro]
|
||||
* build, deploy: Extend CTRL-C coverage on Windows (PowerShell, cmd.exe) [Paulo Castro]
|
||||
|
||||
## 12.48.0 - 2021-08-26
|
||||
|
||||
* Add contract contents at release creation time [Paulo Castro]
|
||||
* Fix env variable to avoid test failures [toochevere]
|
||||
* Add balena.yml handling and `--draft` to `balena deploy` release creation [toochevere]
|
||||
|
||||
## 12.47.0 - 2021-08-19
|
||||
|
||||
* Add deprecation policy checker and --unsupported global flag [Paulo Castro]
|
||||
|
||||
## 12.46.2 - 2021-08-16
|
||||
|
||||
|
||||
<details>
|
||||
<summary> Update dependencies (balena-sdk from v15.36.0 to v15.48.0) [Paulo Castro] </summary>
|
||||
|
||||
> ### balena-sdk-15.48.0 - 2021-08-15
|
||||
>
|
||||
> * Deprecate the release.release_version property [Thodoris Greasidis]
|
||||
> * typings: Add the release versioning properties [Thodoris Greasidis]
|
||||
>
|
||||
> ### balena-sdk-15.47.1 - 2021-08-10
|
||||
>
|
||||
> * Run browser tests using the minified browser bundle [Thodoris Greasidis]
|
||||
> * Move to uglify-js to fix const assignment bug in minified build [Thodoris Greasidis]
|
||||
>
|
||||
> ### balena-sdk-15.47.0 - 2021-08-09
|
||||
>
|
||||
> * typings: Add the release.is_final & is_finalized_at__date properties [Thodoris Greasidis]
|
||||
>
|
||||
> ### balena-sdk-15.46.1 - 2021-07-28
|
||||
>
|
||||
> * apiKey.getAll: Return only NamedUserApiKeys for backwards compatibility [Thodoris Greasidis]
|
||||
>
|
||||
> ### balena-sdk-15.46.0 - 2021-07-27
|
||||
>
|
||||
> * Add email verification & email request methods [Nitish Agarwal]
|
||||
>
|
||||
> ### balena-sdk-15.45.0 - 2021-07-26
|
||||
>
|
||||
> * Update generateProvisioningKey to include keyName [Nitish Agarwal]
|
||||
>
|
||||
> ### balena-sdk-15.44.0 - 2021-07-15
|
||||
>
|
||||
> * typings: Add the subscription.is_active computed term [Thodoris Greasidis]
|
||||
>
|
||||
> ### balena-sdk-15.43.0 - 2021-07-14
|
||||
>
|
||||
> * typings: Add the organization_memebership.effective_seat_role field [Thodoris Greasidis]
|
||||
>
|
||||
> ### balena-sdk-15.42.2 - 2021-07-14
|
||||
>
|
||||
> * tests: Reduce the number of organizations created [Thodoris Greasidis]
|
||||
>
|
||||
> ### balena-sdk-15.42.1 - 2021-07-13
|
||||
>
|
||||
> * tests/api-key: Fix a race condition in the apiKey.create() tests [Thodoris Greasidis]
|
||||
> * Convert the apiKey tests to async-await [Thodoris Greasidis]
|
||||
>
|
||||
> ### balena-sdk-15.42.0 - 2021-07-13
|
||||
>
|
||||
> * models/apiKey: Add getProvisioningApiKeysByApplication() method [Nitish Agarwal]
|
||||
>
|
||||
> ### balena-sdk-15.41.1 - 2021-06-30
|
||||
>
|
||||
> * Delete CODEOWNERS [Thodoris Greasidis]
|
||||
>
|
||||
> ### balena-sdk-15.41.0 - 2021-06-21
|
||||
>
|
||||
> * Add organization__has_private_access_to__device_type typings [Thodoris Greasidis]
|
||||
> * typings: Add organization.has_past_due_invoice_since__date [Thodoris Greasidis]
|
||||
>
|
||||
> ### balena-sdk-15.40.0 - 2021-06-09
|
||||
>
|
||||
> * Add getAllNamedUserApiKeys() in the apiKey model [Thodoris Greasidis]
|
||||
>
|
||||
> ### balena-sdk-15.39.4 - 2021-06-08
|
||||
>
|
||||
> * Add missing modified_at in device type [JSReds]
|
||||
>
|
||||
> ### balena-sdk-15.39.3 - 2021-06-08
|
||||
>
|
||||
> * Fix lint with new linter version [JSReds]
|
||||
>
|
||||
> ### balena-sdk-15.39.2 - 2021-05-27
|
||||
>
|
||||
> * Update TypeScript to v4.3.2 [Thodoris Greasidis]
|
||||
>
|
||||
> ### balena-sdk-15.39.1 - 2021-05-24
|
||||
>
|
||||
> * Update balena-lint to v6 [Thodoris Greasidis]
|
||||
>
|
||||
> ### balena-sdk-15.39.0 - 2021-05-24
|
||||
>
|
||||
> * Add public device types [Tomás Migone]
|
||||
>
|
||||
> ### balena-sdk-15.38.0 - 2021-05-20
|
||||
>
|
||||
> * models/billing: Add changePlan method [Thodoris Greasidis]
|
||||
> * Update DOCUMENTATION about getAllWithDeviceServiceDetails deprecation [Thodoris Greasidis]
|
||||
>
|
||||
> ### balena-sdk-15.37.0 - 2021-05-17
|
||||
>
|
||||
> * Add public organization types [JSReds]
|
||||
>
|
||||
</details>
|
||||
|
||||
## 12.46.1 - 2021-08-16
|
||||
|
||||
|
||||
<details>
|
||||
<summary> preload: Restore support for armv7 with custom preload image [Kyle Harding] </summary>
|
||||
|
||||
> ### balena-preload-10.5.0 - 2021-08-04
|
||||
>
|
||||
> * Remove mutually exclusive args from sfdisk [Kyle Harding]
|
||||
> * Explicitly disable tls to avoid startup delays [Kyle Harding]
|
||||
> * Use custom dind image based on alpine [Kyle Harding]
|
||||
>
|
||||
</details>
|
||||
|
||||
## 12.46.0 - 2021-08-15
|
||||
|
||||
* Add `--draft` option to `balena push` [Felipe Lalanne]
|
||||
|
||||
## 12.45.2 - 2021-08-13
|
||||
|
||||
* push, build: Improve error handling (identify which service failed) [Paulo Castro]
|
||||
|
||||
## 12.45.1 - 2021-08-11
|
||||
|
||||
* envs, env add: Fix "Application is ambiguous" when using device UUID [Paulo Castro]
|
||||
|
||||
## 12.45.0 - 2021-08-09
|
||||
|
||||
* Rename applications to fleets (stage 1). See: https://git.io/JRuZr [Paulo Castro]
|
||||
|
@ -259,3 +259,12 @@ gotchas to bear in mind:
|
||||
`node_modules/balena-sdk/node_modules/balena-errors`
|
||||
In the case of subclasses of `TypedError`, a string comparison may be used instead:
|
||||
`error.name === 'BalenaApplicationNotFound'`
|
||||
|
||||
## Further debugging notes
|
||||
|
||||
* If you need to selectively run specific tests, `it.only` will not work in cases when authorization is required as part of the test cycle. In order to target specific tests, control execution via `.mocharc.js` instead. Here is an example of targeting the `deploy` tests.
|
||||
|
||||
replace: `spec: 'tests/**/*.spec.ts',`
|
||||
|
||||
with: `spec: ['tests/auth/*.spec.ts', 'tests/**/deploy.spec.ts'],`
|
||||
|
||||
|
@ -1,6 +1,10 @@
|
||||
# balena CLI Installation Instructions for Linux
|
||||
|
||||
These instructions are suitable for most Linux distributions on Intel x86, except notably for **Linux Alpine** or **Busybox**. For these distros or for the ARM architecture, follow the [NPM Installation](./INSTALL-ADVANCED.md#npm-installation) method.
|
||||
These instructions are suitable for most Linux distributions on Intel x86, such as
|
||||
Ubuntu, Debian, Fedora, Arch Linux and other glibc-based distributions.
|
||||
For the ARM architecture and for Linux distributions not based on glibc, such as
|
||||
Alpine Linux, follow the [NPM Installation](./INSTALL-ADVANCED.md#npm-installation)
|
||||
method.
|
||||
|
||||
Selected operating system: **Linux**
|
||||
|
||||
|
@ -10,16 +10,14 @@ Selected operating system: **macOS**
|
||||
Look for a file name that ends with "-installer.pkg":
|
||||
`balena-cli-vX.Y.Z-macOS-x64-installer.pkg`
|
||||
|
||||
2. Double click the downloaded file to run the installer. After the installation completes,
|
||||
close and re-open any open [command
|
||||
terminal](https://www.balena.io/docs/reference/cli/#choosing-a-shell-command-promptterminal)
|
||||
windows (so that the changes made by the installer to the PATH environment variable can take
|
||||
effect).
|
||||
2. Double click on the downloaded file to run the installer and follow the installer's
|
||||
instructions.
|
||||
|
||||
3. Check that the installation was successful by running the following commands on a
|
||||
command terminal:
|
||||
* `balena version` - should print the CLI's version
|
||||
* `balena help` - should print a list of available commands
|
||||
3. Check that the installation was successful:
|
||||
- [Open the Terminal
|
||||
app](https://support.apple.com/en-gb/guide/terminal/apd5265185d-f365-44cb-8b09-71a064a42125/mac).
|
||||
- On the terminal prompt, type `balena version` and hit Enter. It should display
|
||||
the version of the balena CLI that you have installed.
|
||||
|
||||
No further steps are required to run most CLI commands. The `balena ssh`, `build`, `deploy`
|
||||
and `preload` commands may require additional software to be installed, as described
|
||||
|
@ -10,16 +10,14 @@ Selected operating system: **Windows**
|
||||
Look for a file name that ends with "-installer.exe":
|
||||
`balena-cli-vX.Y.Z-windows-x64-installer.exe`
|
||||
|
||||
2. Double click the downloaded file to run the installer. After the installation completes,
|
||||
close and re-open any open [command
|
||||
terminal](https://www.balena.io/docs/reference/cli/#choosing-a-shell-command-promptterminal)
|
||||
windows (so that the changes made by the installer to the PATH environment variable can take
|
||||
effect).
|
||||
2. Double click on the downloaded file to run the installer and follow the installer's
|
||||
instructions.
|
||||
|
||||
3. Check that the installation was successful by running the following commands on a
|
||||
command terminal:
|
||||
* `balena version` - should print the CLI's version
|
||||
* `balena help` - should print a list of available commands
|
||||
3. Check that the installation was successful:
|
||||
- Click on the Windows Start Menu, type PowerShell, and then click
|
||||
on Windows PowerShell.
|
||||
- On the command prompt, type `balena version` and hit Enter. It should display
|
||||
the version of the balena CLI that you have installed.
|
||||
|
||||
No further steps are required to run most CLI commands. The `balena ssh`, `scan`, `build`,
|
||||
`deploy` and `preload` commands may require additional software to be installed, as
|
||||
|
18
README.md
18
README.md
@ -156,14 +156,18 @@ of major, minor and patch version releases.
|
||||
|
||||
The latest release of a major version of the balena CLI will remain compatible with
|
||||
the balenaCloud backend services for at least one year from the date when the
|
||||
following major version is released. For example, balena CLI v10.17.5, as the
|
||||
latest v10 release, would remain compatible with the balenaCloud backend for one
|
||||
year from the date when v11.0.0 is released.
|
||||
following major version is released. For example, balena CLI v11.36.0, as the
|
||||
latest v11 release, would remain compatible with the balenaCloud backend for one
|
||||
year from the date when v12.0.0 was released.
|
||||
|
||||
At the end of this period, the older major version is considered deprecated and
|
||||
some of the functionality that depends on balenaCloud services may stop working
|
||||
at any time.
|
||||
Users are encouraged to regularly update the balena CLI to the latest version.
|
||||
Half way through to that period (6 months after the release of the next major
|
||||
version), older major versions of the balena CLI will start printing a deprecation
|
||||
warning message when it is used interactively (when `stderr` is attached to a TTY
|
||||
device file). At the end of that period, older major versions will exit with an
|
||||
error message unless the `--unsupported` flag is used. This behavior was
|
||||
introduced in CLI version 12.47.0 and is also documented by `balena help`.
|
||||
To take advantage of the latest backend features and ensure compatibility, users
|
||||
are encouraged to regularly update the balena CLI to the latest version.
|
||||
|
||||
## Contributing (including editing documentation files)
|
||||
|
||||
|
@ -22,25 +22,26 @@ import * as archiver from 'archiver';
|
||||
import * as Bluebird from 'bluebird';
|
||||
import { execFile } from 'child_process';
|
||||
import * as filehound from 'filehound';
|
||||
import { Stats } from 'fs';
|
||||
import * as fs from 'fs-extra';
|
||||
import * as klaw from 'klaw';
|
||||
import * as _ from 'lodash';
|
||||
import * as path from 'path';
|
||||
import * as rimraf from 'rimraf';
|
||||
import * as semver from 'semver';
|
||||
import * as util from 'util';
|
||||
import * as klaw from 'klaw';
|
||||
import { Stats } from 'fs';
|
||||
import { promisify } from 'util';
|
||||
|
||||
import { stripIndent } from '../lib/utils/lazy';
|
||||
import { stripIndent } from '../build/utils/lazy';
|
||||
import {
|
||||
diffLines,
|
||||
getSubprocessStdout,
|
||||
loadPackageJson,
|
||||
ROOT,
|
||||
StdOutTap,
|
||||
whichSpawn,
|
||||
} from './utils';
|
||||
|
||||
const execFileAsync = promisify(execFile);
|
||||
|
||||
export const packageJSON = loadPackageJson();
|
||||
export const version = 'v' + packageJSON.version;
|
||||
const arch = process.arch;
|
||||
@ -246,7 +247,17 @@ async function testPkg() {
|
||||
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)
|
||||
const stdout = await getSubprocessStdout(pkgBalenaPath, ['version', '-j']);
|
||||
let { stdout, stderr } = await execFileAsync(pkgBalenaPath, [
|
||||
'version',
|
||||
'-j',
|
||||
]);
|
||||
const { filterCliOutputForTests } = await import('../tests/helpers');
|
||||
const filtered = filterCliOutputForTests({
|
||||
err: stderr.split(/\r?\n/),
|
||||
out: stdout.split(/\r?\n/),
|
||||
});
|
||||
stdout = filtered.out.join('\n');
|
||||
stderr = filtered.err.join('\n');
|
||||
let pkgNodeVersion = '';
|
||||
let pkgNodeMajorVersion = 0;
|
||||
try {
|
||||
@ -263,6 +274,10 @@ async function testPkg() {
|
||||
`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)');
|
||||
}
|
||||
|
||||
@ -411,8 +426,6 @@ async function renameInstallerFiles() {
|
||||
async function signWindowsInstaller() {
|
||||
if (process.env.CSC_LINK && process.env.CSC_KEY_PASSWORD) {
|
||||
const exeName = renamedOclifInstallers[process.platform];
|
||||
const execFileAsync = util.promisify<string, string[], void>(execFile);
|
||||
|
||||
console.log(`Signing installer "${exeName}"`);
|
||||
await execFileAsync(MSYS2_BASH, [
|
||||
'sign-exe.sh',
|
||||
|
@ -82,6 +82,14 @@ const capitanoDoc = {
|
||||
'build/commands/device/shutdown.js',
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Releases',
|
||||
files: [
|
||||
'build/commands/releases.js',
|
||||
'build/commands/release/index.js',
|
||||
'build/commands/release/finalize.js',
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Environment Variables',
|
||||
files: [
|
||||
|
@ -101,7 +101,7 @@ async function printMarkdown() {
|
||||
console.log(await renderMarkdown());
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
process.exit(1);
|
||||
process.exitCode = 1;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -41,17 +41,25 @@ function checkNpmVersion() {
|
||||
// the reason is that it would unnecessarily prevent end users from
|
||||
// using npm v6.4.1 that ships with Node 8. (It is OK for the
|
||||
// shrinkwrap file to get damaged if it is not going to be reused.)
|
||||
console.error(`\
|
||||
-------------------------------------------------------------------------------
|
||||
throw new Error(`\
|
||||
-----------------------------------------------------------------------------
|
||||
Error: npm version '${npmVersion}' detected. Please upgrade to npm v${requiredVersion} or later
|
||||
because of a bug that causes the 'npm-shrinkwrap.json' file to be damaged.
|
||||
At this point, however, your 'npm-shrinkwrap.json' file has already been
|
||||
damaged. Please revert it to the master branch state with a command such as:
|
||||
"git checkout master -- npm-shrinkwrap.json"
|
||||
Then re-run "npm install" using npm version ${requiredVersion} or later.
|
||||
-------------------------------------------------------------------------------`);
|
||||
process.exit(1);
|
||||
-----------------------------------------------------------------------------`);
|
||||
}
|
||||
}
|
||||
|
||||
checkNpmVersion();
|
||||
function main() {
|
||||
try {
|
||||
checkNpmVersion();
|
||||
} catch (e) {
|
||||
console.error(e.message || e);
|
||||
process.exitCode = 1;
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
|
@ -54,9 +54,7 @@ export async function release() {
|
||||
try {
|
||||
await createGitHubRelease();
|
||||
} catch (err) {
|
||||
console.error('Release failed');
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
throw new Error(`Error creating GitHub release:\n${err}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -35,11 +35,6 @@ process.env.DEBUG = ['0', 'no', 'false', '', undefined].includes(
|
||||
? ''
|
||||
: '1';
|
||||
|
||||
function exitWithError(error: Error | string): never {
|
||||
console.error(`Error: ${error}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Trivial command-line parser. Check whether the command-line argument is one
|
||||
* of the following strings, then call the appropriate functions:
|
||||
@ -49,12 +44,12 @@ function exitWithError(error: Error | string): never {
|
||||
*
|
||||
* @param args Arguments to parse (default is process.argv.slice(2))
|
||||
*/
|
||||
export async function run(args?: string[]) {
|
||||
async function parse(args?: string[]) {
|
||||
args = args || process.argv.slice(2);
|
||||
console.log(`automation/run.ts process.argv=[${process.argv}]\n`);
|
||||
console.log(`automation/run.ts args=[${args}]`);
|
||||
console.error(`[debug] automation/run.ts process.argv=[${process.argv}]`);
|
||||
console.error(`[debug] automation/run.ts args=[${args}]`);
|
||||
if (_.isEmpty(args)) {
|
||||
return exitWithError('missing command-line arguments');
|
||||
throw new Error('missing command-line arguments');
|
||||
}
|
||||
const commands: { [cmd: string]: () => void | Promise<void> } = {
|
||||
'build:installer': buildOclifInstaller,
|
||||
@ -66,7 +61,7 @@ export async function run(args?: string[]) {
|
||||
};
|
||||
for (const arg of args) {
|
||||
if (!commands.hasOwnProperty(arg)) {
|
||||
return exitWithError(`command unknown: ${arg}`);
|
||||
throw new Error(`command unknown: ${arg}`);
|
||||
}
|
||||
}
|
||||
|
||||
@ -90,9 +85,22 @@ export async function run(args?: string[]) {
|
||||
const cmdFunc = commands[arg];
|
||||
await cmdFunc();
|
||||
} catch (err) {
|
||||
return exitWithError(`"${arg}": ${err}`);
|
||||
if (typeof err === 'object') {
|
||||
err.message = `"${arg}": ${err.message}`;
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** See jsdoc for parse() function above */
|
||||
export async function run(args?: string[]) {
|
||||
try {
|
||||
await parse(args);
|
||||
} catch (e) {
|
||||
console.error(e.message ? `Error: ${e.message}` : e);
|
||||
process.exitCode = 1;
|
||||
}
|
||||
}
|
||||
|
||||
run();
|
||||
|
@ -11,8 +11,7 @@ const validateChangeType = (maybeChangeType: string = 'minor') => {
|
||||
case 'major':
|
||||
return maybeChangeType;
|
||||
default:
|
||||
console.error(`Invalid change type: '${maybeChangeType}'`);
|
||||
return process.exit(1);
|
||||
throw new Error(`Invalid change type: '${maybeChangeType}'`);
|
||||
}
|
||||
};
|
||||
|
||||
@ -65,24 +64,17 @@ const getUpstreams = async () => {
|
||||
return upstream;
|
||||
};
|
||||
|
||||
const printUsage = (upstreams: Upstream[], upstreamName: string) => {
|
||||
console.error(
|
||||
`
|
||||
const getUsage = (upstreams: Upstream[], upstreamName: string) => `
|
||||
Usage: npm run update ${upstreamName} $version [$changeType=minor]
|
||||
|
||||
Upstream names: ${upstreams.map(({ repo }) => repo).join(', ')}
|
||||
`,
|
||||
);
|
||||
return process.exit(1);
|
||||
};
|
||||
`;
|
||||
|
||||
// TODO: Drop the wrapper function once we move to TS 3.8,
|
||||
// which will support top level await.
|
||||
async function main() {
|
||||
async function $main() {
|
||||
const upstreams = await getUpstreams();
|
||||
|
||||
if (process.argv.length < 3) {
|
||||
return printUsage(upstreams, '$upstreamName');
|
||||
throw new Error(getUsage(upstreams, '$upstreamName'));
|
||||
}
|
||||
|
||||
const upstreamName = process.argv[2];
|
||||
@ -90,16 +82,15 @@ async function main() {
|
||||
const upstream = upstreams.find((v) => v.repo === upstreamName);
|
||||
|
||||
if (!upstream) {
|
||||
console.error(
|
||||
throw new Error(
|
||||
`Invalid upstream name '${upstreamName}', valid options: ${upstreams
|
||||
.map(({ repo }) => repo)
|
||||
.join(', ')}`,
|
||||
);
|
||||
return process.exit(1);
|
||||
}
|
||||
|
||||
if (process.argv.length < 4) {
|
||||
printUsage(upstreams, upstreamName);
|
||||
throw new Error(getUsage(upstreams, upstreamName));
|
||||
}
|
||||
|
||||
const packageName = upstream.module || upstream.repo;
|
||||
@ -108,8 +99,7 @@ async function main() {
|
||||
await run(`npm install ${packageName}@${process.argv[3]}`);
|
||||
const newVersion = await getVersion(packageName);
|
||||
if (newVersion === oldVersion) {
|
||||
console.error(`Already on version '${newVersion}'`);
|
||||
return process.exit(1);
|
||||
throw new Error(`Already on version '${newVersion}'`);
|
||||
}
|
||||
|
||||
console.log(`Updated ${upstreamName} from ${oldVersion} to ${newVersion}`);
|
||||
@ -137,4 +127,13 @@ async function main() {
|
||||
);
|
||||
}
|
||||
|
||||
async function main() {
|
||||
try {
|
||||
await $main();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
process.exitCode = 1;
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
|
@ -21,22 +21,6 @@ import * as path from 'path';
|
||||
|
||||
export const ROOT = path.join(__dirname, '..');
|
||||
|
||||
const nodeEngineWarn = `\
|
||||
------------------------------------------------------------------------------
|
||||
Warning: Node version "v14.x.x" does not match required versions ">=10.20.0 <13.0.0".
|
||||
This may cause unexpected behavior. To upgrade Node, visit:
|
||||
https://nodejs.org/en/download/
|
||||
------------------------------------------------------------------------------
|
||||
`;
|
||||
const nodeEngineWarnArray = nodeEngineWarn.split('\n').filter((l) => l);
|
||||
|
||||
export function matchesNodeEngineVersionWarn(line: string) {
|
||||
line = line.replace(/"v14\.\d{1,3}\.\d{1,3}"/, '"v14.x.x"');
|
||||
return (
|
||||
line === nodeEngineWarn || nodeEngineWarnArray.includes(line.trimEnd())
|
||||
);
|
||||
}
|
||||
|
||||
/** Tap and buffer this process' stdout and stderr */
|
||||
export class StdOutTap {
|
||||
public stdoutBuf: string[] = [];
|
||||
@ -104,60 +88,6 @@ export function loadPackageJson() {
|
||||
return require(path.join(ROOT, 'package.json'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Run the executable at execPath as a child process, and resolve a promise
|
||||
* to the executable's stdout output as a string. Reject the promise if
|
||||
* anything is printed to stderr, or if the child process exits with a
|
||||
* non-zero exit code.
|
||||
* @param execPath Executable path
|
||||
* @param args Command-line argument for the executable
|
||||
*/
|
||||
export async function getSubprocessStdout(
|
||||
execPath: string,
|
||||
args: string[],
|
||||
): Promise<string> {
|
||||
const child = spawn(execPath, args);
|
||||
return new Promise((resolve, reject) => {
|
||||
let stdout = '';
|
||||
child.stdout.on('error', reject);
|
||||
child.stderr.on('error', reject);
|
||||
child.stdout.on('data', (data: Buffer) => {
|
||||
try {
|
||||
stdout = data.toString();
|
||||
} catch (err) {
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
child.stderr.on('data', (data: Buffer) => {
|
||||
try {
|
||||
const stderr = data.toString();
|
||||
|
||||
// ignore any debug lines, but ensure that we parse
|
||||
// every line provided to the stderr stream
|
||||
const lines = _.filter(
|
||||
stderr.trim().split(/\r?\n/),
|
||||
(line) =>
|
||||
!line.startsWith('[debug]') && !matchesNodeEngineVersionWarn(line),
|
||||
);
|
||||
if (lines.length > 0) {
|
||||
reject(
|
||||
new Error(`"${execPath}": non-empty stderr "${lines.join('\n')}"`),
|
||||
);
|
||||
}
|
||||
} catch (err) {
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
child.on('exit', (code: number) => {
|
||||
if (code) {
|
||||
reject(new Error(`"${execPath}": non-zero exit code "${code}"`));
|
||||
} else {
|
||||
resolve(stdout);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Error handling wrapper around the npm `which` package:
|
||||
* "Like the unix which utility. Finds the first instance of a specified
|
||||
|
@ -8,7 +8,7 @@ _balena() {
|
||||
local context state line curcontext="$curcontext"
|
||||
|
||||
# Valid top-level completions
|
||||
main_commands=( apps build deploy envs fleets join keys leave login logout logs note orgs preload push scan settings ssh support tags tunnel version whoami api-key app app config device device devices env fleet fleet internal key key local os tag util )
|
||||
main_commands=( apps build deploy envs fleets join keys leave login logout logs note orgs preload push releases scan settings ssh support tags tunnel version whoami api-key app app config device device devices env fleet fleet internal key key local os release release tag util )
|
||||
# Sub-completions
|
||||
api_key_cmds=( generate )
|
||||
app_cmds=( create purge rename restart rm )
|
||||
@ -21,6 +21,7 @@ _balena() {
|
||||
key_cmds=( add rm )
|
||||
local_cmds=( configure flash )
|
||||
os_cmds=( build-config configure download initialize versions )
|
||||
release_cmds=( finalize )
|
||||
tag_cmds=( rm set )
|
||||
|
||||
|
||||
@ -73,6 +74,9 @@ _balena_sec_cmds() {
|
||||
"os")
|
||||
_describe -t os_cmds 'os_cmd' os_cmds "$@" && ret=0
|
||||
;;
|
||||
"release")
|
||||
_describe -t release_cmds 'release_cmd' release_cmds "$@" && ret=0
|
||||
;;
|
||||
"tag")
|
||||
_describe -t tag_cmds 'tag_cmd' tag_cmds "$@" && ret=0
|
||||
;;
|
||||
|
@ -7,7 +7,7 @@ _balena_complete()
|
||||
local cur prev
|
||||
|
||||
# Valid top-level completions
|
||||
main_commands="apps build deploy envs fleets join keys leave login logout logs note orgs preload push scan settings ssh support tags tunnel version whoami api-key app app config device device devices env fleet fleet internal key key local os tag util"
|
||||
main_commands="apps build deploy envs fleets join keys leave login logout logs note orgs preload push releases scan settings ssh support tags tunnel version whoami api-key app app config device device devices env fleet fleet internal key key local os release release tag util"
|
||||
# Sub-completions
|
||||
api_key_cmds="generate"
|
||||
app_cmds="create purge rename restart rm"
|
||||
@ -20,6 +20,7 @@ _balena_complete()
|
||||
key_cmds="add rm"
|
||||
local_cmds="configure flash"
|
||||
os_cmds="build-config configure download initialize versions"
|
||||
release_cmds="finalize"
|
||||
tag_cmds="rm set"
|
||||
|
||||
|
||||
@ -67,6 +68,9 @@ _balena_complete()
|
||||
os)
|
||||
COMPREPLY=( $(compgen -W "$os_cmds" -- $cur) )
|
||||
;;
|
||||
release)
|
||||
COMPREPLY=( $(compgen -W "$release_cmds" -- $cur) )
|
||||
;;
|
||||
tag)
|
||||
COMPREPLY=( $(compgen -W "$tag_cmds" -- $cur) )
|
||||
;;
|
||||
|
155
doc/cli.markdown
155
doc/cli.markdown
@ -144,14 +144,18 @@ of major, minor and patch version releases.
|
||||
|
||||
The latest release of a major version of the balena CLI will remain compatible with
|
||||
the balenaCloud backend services for at least one year from the date when the
|
||||
following major version is released. For example, balena CLI v10.17.5, as the
|
||||
latest v10 release, would remain compatible with the balenaCloud backend for one
|
||||
year from the date when v11.0.0 is released.
|
||||
following major version is released. For example, balena CLI v11.36.0, as the
|
||||
latest v11 release, would remain compatible with the balenaCloud backend for one
|
||||
year from the date when v12.0.0 was released.
|
||||
|
||||
At the end of this period, the older major version is considered deprecated and
|
||||
some of the functionality that depends on balenaCloud services may stop working
|
||||
at any time.
|
||||
Users are encouraged to regularly update the balena CLI to the latest version.
|
||||
Half way through to that period (6 months after the release of the next major
|
||||
version), older major versions of the balena CLI will start printing a deprecation
|
||||
warning message when it is used interactively (when `stderr` is attached to a TTY
|
||||
device file). At the end of that period, older major versions will exit with an
|
||||
error message unless the `--unsupported` flag is used. This behavior was
|
||||
introduced in CLI version 12.47.0 and is also documented by `balena help`.
|
||||
To take advantage of the latest backend features and ensure compatibility, users
|
||||
are encouraged to regularly update the balena CLI to the latest version.
|
||||
|
||||
|
||||
# CLI Command Reference
|
||||
@ -203,6 +207,12 @@ Users are encouraged to regularly update the balena CLI to the latest version.
|
||||
- [device rm <uuid(s)>](#device-rm-uuid-s)
|
||||
- [device shutdown <uuid>](#device-shutdown-uuid)
|
||||
|
||||
- Releases
|
||||
|
||||
- [releases <fleet>](#releases-fleet)
|
||||
- [release <commitorid>](#release-commitorid)
|
||||
- [release finalize <commitorid>](#release-finalize-commitorid)
|
||||
|
||||
- Environment Variables
|
||||
|
||||
- [envs](#envs)
|
||||
@ -1290,6 +1300,80 @@ the uuid of the device to shutdown
|
||||
|
||||
force action if the update lock is set
|
||||
|
||||
# Releases
|
||||
|
||||
## releases <fleet>
|
||||
|
||||
List all releases of the given fleet.
|
||||
|
||||
Fleets may be specified by fleet name or slug. Slugs are recommended because
|
||||
they are unique and unambiguous. Slugs can be listed with the `balena fleets`
|
||||
command. Note that slugs may change if the fleet is renamed. Fleet names are
|
||||
not unique and may result in "Fleet is ambiguous" errors at any time (even if
|
||||
"it used to work in the past"), for example if the name clashes with a newly
|
||||
created public/open fleet, or with fleets from other balena accounts that you
|
||||
may be invited to join under any role. For this reason, fleet names are
|
||||
especially discouraged in scripts (e.g. CI environments).
|
||||
|
||||
Examples:
|
||||
|
||||
$ balena releases myorg/myfleet
|
||||
|
||||
### Arguments
|
||||
|
||||
#### FLEET
|
||||
|
||||
fleet name or slug
|
||||
|
||||
### Options
|
||||
|
||||
## release <commitOrId>
|
||||
|
||||
|
||||
|
||||
Examples:
|
||||
|
||||
$ balena release a777f7345fe3d655c1c981aa642e5555
|
||||
$ balena release 1234567
|
||||
|
||||
### Arguments
|
||||
|
||||
#### COMMITORID
|
||||
|
||||
the commit or ID of the release to get information
|
||||
|
||||
### Options
|
||||
|
||||
#### -c, --composition
|
||||
|
||||
Return the release composition
|
||||
|
||||
## release finalize <commitOrId>
|
||||
|
||||
Finalize a release. Releases can be "draft" or "final", and this command
|
||||
changes a draft release into a final release. Draft releases can be created
|
||||
with the `--draft` option of the `balena build` or `balena deploy`
|
||||
commands.
|
||||
|
||||
Draft releases are not automatically deployed to devices tracking the latest
|
||||
release. For a draft release to be deployed to a device, the device should be
|
||||
explicity pinned to that release. Conversely, final releases may trigger immediate
|
||||
deployment to unpinned devices (subject to a device's polling period) and, for
|
||||
this reason, final releases cannot be changed back to draft status.
|
||||
|
||||
Examples:
|
||||
|
||||
$ balena release finalize a777f7345fe3d655c1c981aa642e5555
|
||||
$ balena release finalize 1234567
|
||||
|
||||
### Arguments
|
||||
|
||||
#### COMMITORID
|
||||
|
||||
the commit or ID of the release to finalize
|
||||
|
||||
### Options
|
||||
|
||||
# Environment Variables
|
||||
|
||||
## envs
|
||||
@ -2493,8 +2577,11 @@ the wifi key to use (used only if --network is set to wifi)
|
||||
|
||||
## config inject <file>
|
||||
|
||||
Inject a config.json file to the mounted filesystem,
|
||||
e.g. the SD card of a provisioned device or balenaOS image.
|
||||
Inject a config.json file to a mounted filesystem, e.g. the SD card of a
|
||||
provisioned device or balenaOS image.
|
||||
|
||||
Note: if using a private/custom device type, please ensure you are logged in
|
||||
('balena login' command). Public device types do not require logging in.
|
||||
|
||||
Examples:
|
||||
|
||||
@ -2922,22 +3009,29 @@ Don't convert line endings from CRLF (Windows format) to LF (Unix format).
|
||||
|
||||
Have each service use its own .dockerignore file. See "balena help push".
|
||||
|
||||
#### -G, --nogitignore
|
||||
|
||||
No-op (default behavior) since balena CLI v12.0.0. See "balena help push".
|
||||
|
||||
#### -g, --gitignore
|
||||
|
||||
Consider .gitignore files in addition to the .dockerignore file. This reverts
|
||||
to the CLI v11 behavior/implementation (deprecated) if compatibility is
|
||||
required until your project can be adapted.
|
||||
|
||||
#### -G, --nogitignore
|
||||
|
||||
No-op (default behavior) since balena CLI v12.0.0. See "balena help push".
|
||||
|
||||
#### --release-tag RELEASE-TAG
|
||||
|
||||
Set release tags if the image build is successful (balenaCloud only). Multiple
|
||||
arguments may be provided, alternating tag keys and values (see examples).
|
||||
Hint: Empty values may be specified with "" (bash, cmd.exe) or '""' (PowerShell).
|
||||
|
||||
#### --draft
|
||||
|
||||
Instruct the builder to create the release as a draft. Draft releases are ignored
|
||||
by the 'track latest' release policy but can be used through release pinning.
|
||||
Draft releases can be marked as final through the API. Releases are created
|
||||
as final by default unless this option is given.
|
||||
|
||||
# Settings
|
||||
|
||||
## settings
|
||||
@ -3148,14 +3242,14 @@ Consider .gitignore files in addition to the .dockerignore file. This reverts
|
||||
to the CLI v11 behavior/implementation (deprecated) if compatibility is required
|
||||
until your project can be adapted.
|
||||
|
||||
#### -m, --multi-dockerignore
|
||||
|
||||
Have each service use its own .dockerignore file. See "balena help build".
|
||||
|
||||
#### -G, --nogitignore
|
||||
|
||||
No-op (default behavior) since balena CLI v12.0.0. See "balena help build".
|
||||
|
||||
#### -m, --multi-dockerignore
|
||||
|
||||
Have each service use its own .dockerignore file. See "balena help build".
|
||||
|
||||
#### --noparent-check
|
||||
|
||||
Disable project validation check of 'docker-compose.yml' file in parent folder
|
||||
@ -3174,11 +3268,13 @@ Don't convert line endings from CRLF (Windows format) to LF (Unix format).
|
||||
|
||||
#### -n, --projectName PROJECTNAME
|
||||
|
||||
Specify an alternate project name; default is the directory name
|
||||
Name prefix for locally built images. This is the 'projectName' portion
|
||||
in 'projectName_serviceName:tag'. The default is the directory name.
|
||||
|
||||
#### -t, --tag TAG
|
||||
|
||||
The alias to the generated image
|
||||
Tag locally built Docker images. This is the 'tag' portion
|
||||
in 'projectName_serviceName:tag'. The default is 'latest'.
|
||||
|
||||
#### -B, --buildArg BUILDARG
|
||||
|
||||
@ -3358,6 +3454,13 @@ Set release tags if the image deployment is successful. Multiple
|
||||
arguments may be provided, alternating tag keys and values (see examples).
|
||||
Hint: Empty values may be specified with "" (bash, cmd.exe) or '""' (PowerShell).
|
||||
|
||||
#### --draft
|
||||
|
||||
Deploy the release as a draft. Draft releases are ignored
|
||||
by the 'track latest' release policy but can be used through release pinning.
|
||||
Draft releases can be marked as final through the API. Releases are created
|
||||
as final by default unless this option is given.
|
||||
|
||||
#### -e, --emulated
|
||||
|
||||
Use QEMU for ARM architecture emulation during the image build
|
||||
@ -3380,14 +3483,14 @@ Consider .gitignore files in addition to the .dockerignore file. This reverts
|
||||
to the CLI v11 behavior/implementation (deprecated) if compatibility is required
|
||||
until your project can be adapted.
|
||||
|
||||
#### -m, --multi-dockerignore
|
||||
|
||||
Have each service use its own .dockerignore file. See "balena help build".
|
||||
|
||||
#### -G, --nogitignore
|
||||
|
||||
No-op (default behavior) since balena CLI v12.0.0. See "balena help build".
|
||||
|
||||
#### -m, --multi-dockerignore
|
||||
|
||||
Have each service use its own .dockerignore file. See "balena help build".
|
||||
|
||||
#### --noparent-check
|
||||
|
||||
Disable project validation check of 'docker-compose.yml' file in parent folder
|
||||
@ -3406,11 +3509,13 @@ Don't convert line endings from CRLF (Windows format) to LF (Unix format).
|
||||
|
||||
#### -n, --projectName PROJECTNAME
|
||||
|
||||
Specify an alternate project name; default is the directory name
|
||||
Name prefix for locally built images. This is the 'projectName' portion
|
||||
in 'projectName_serviceName:tag'. The default is the directory name.
|
||||
|
||||
#### -t, --tag TAG
|
||||
|
||||
The alias to the generated image
|
||||
Tag locally built Docker images. This is the 'tag' portion
|
||||
in 'projectName_serviceName:tag'. The default is 'latest'.
|
||||
|
||||
#### -B, --buildArg BUILDARG
|
||||
|
||||
|
46
lib/app.ts
46
lib/app.ts
@ -16,8 +16,14 @@
|
||||
*/
|
||||
|
||||
import * as packageJSON from '../package.json';
|
||||
import {
|
||||
AppOptions,
|
||||
checkDeletedCommand,
|
||||
preparseArgs,
|
||||
unsupportedFlag,
|
||||
} from './preparser';
|
||||
import { CliSettings } from './utils/bootstrap';
|
||||
import { onceAsync, stripIndent } from './utils/lazy';
|
||||
import { onceAsync } from './utils/lazy';
|
||||
|
||||
/**
|
||||
* Sentry.io setup
|
||||
@ -27,6 +33,7 @@ export const setupSentry = onceAsync(async () => {
|
||||
const config = await import('./config');
|
||||
const Sentry = await import('@sentry/node');
|
||||
Sentry.init({
|
||||
autoSessionTracking: false,
|
||||
dsn: config.sentryDsn,
|
||||
release: packageJSON.version,
|
||||
});
|
||||
@ -43,13 +50,8 @@ export const setupSentry = onceAsync(async () => {
|
||||
async function checkNodeVersion() {
|
||||
const validNodeVersions = packageJSON.engines.node;
|
||||
if (!(await import('semver')).satisfies(process.version, validNodeVersions)) {
|
||||
console.warn(stripIndent`
|
||||
------------------------------------------------------------------------------
|
||||
Warning: Node version "${process.version}" does not match required versions "${validNodeVersions}".
|
||||
This may cause unexpected behavior. To upgrade Node, visit:
|
||||
https://nodejs.org/en/download/
|
||||
------------------------------------------------------------------------------
|
||||
`);
|
||||
const { getNodeEngineVersionWarn } = await import('./utils/messages');
|
||||
console.warn(getNodeEngineVersionWarn(process.version, validNodeVersions));
|
||||
}
|
||||
}
|
||||
|
||||
@ -93,10 +95,20 @@ async function init() {
|
||||
}
|
||||
|
||||
/** Execute the oclif parser and the CLI command. */
|
||||
async function oclifRun(
|
||||
command: string[],
|
||||
options: import('./preparser').AppOptions,
|
||||
) {
|
||||
async function oclifRun(command: string[], options: AppOptions) {
|
||||
let deprecationPromise: Promise<void>;
|
||||
// check and enforce the CLI's deprecation policy
|
||||
if (unsupportedFlag || process.env.BALENARC_UNSUPPORTED) {
|
||||
deprecationPromise = Promise.resolve();
|
||||
} else {
|
||||
const { DeprecationChecker } = await import('./deprecation');
|
||||
const deprecationChecker = new DeprecationChecker(packageJSON.version);
|
||||
// warnAndAbortIfDeprecated uses previously cached data only
|
||||
await deprecationChecker.warnAndAbortIfDeprecated();
|
||||
// checkForNewReleasesIfNeeded may query the npm registry
|
||||
deprecationPromise = deprecationChecker.checkForNewReleasesIfNeeded();
|
||||
}
|
||||
|
||||
const runPromise = (async function (shouldFlush: boolean) {
|
||||
const { CustomMain } = await import('./utils/oclif-utils');
|
||||
let isEEXIT = false;
|
||||
@ -130,14 +142,12 @@ async function oclifRun(
|
||||
})(!options.noFlush);
|
||||
|
||||
const { trackPromise } = await import('./hooks/prerun/track');
|
||||
await Promise.all([trackPromise, runPromise]);
|
||||
|
||||
await Promise.all([trackPromise, deprecationPromise, runPromise]);
|
||||
}
|
||||
|
||||
/** CLI entrypoint. Called by the `bin/balena` and `bin/balena-dev` scripts. */
|
||||
export async function run(
|
||||
cliArgs = process.argv,
|
||||
options: import('./preparser').AppOptions = {},
|
||||
) {
|
||||
export async function run(cliArgs = process.argv, options: AppOptions = {}) {
|
||||
try {
|
||||
const { normalizeEnvVars, pkgExec } = await import('./utils/bootstrap');
|
||||
normalizeEnvVars();
|
||||
@ -150,8 +160,6 @@ export async function run(
|
||||
|
||||
await init();
|
||||
|
||||
const { preparseArgs, checkDeletedCommand } = await import('./preparser');
|
||||
|
||||
// Look for commands that have been removed and if so, exit with a notice
|
||||
checkDeletedCommand(cliArgs.slice(2));
|
||||
|
||||
|
@ -120,7 +120,7 @@ export class FleetRenameCmd extends Command {
|
||||
} catch (e) {
|
||||
// BalenaRequestError: Request error: "organization" and "app_name" must be unique.
|
||||
if ((e.message || '').toLowerCase().includes('unique')) {
|
||||
throw new ExpectedError(`Error: fleet ${params.fleet} already exists.`);
|
||||
throw new ExpectedError(`Error: fleet ${newName} already exists.`);
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
|
@ -239,7 +239,12 @@ ${dockerignoreHelp}
|
||||
) {
|
||||
const { loadProject } = await import('../utils/compose_ts');
|
||||
|
||||
const project = await loadProject(logger, composeOpts);
|
||||
const project = await loadProject(
|
||||
logger,
|
||||
composeOpts,
|
||||
undefined,
|
||||
opts.buildOpts.t,
|
||||
);
|
||||
|
||||
const appType = (opts.app?.application_type as ApplicationType[])?.[0];
|
||||
if (
|
||||
@ -266,7 +271,7 @@ ${dockerignoreHelp}
|
||||
inlineLogs: composeOpts.inlineLogs,
|
||||
convertEol: composeOpts.convertEol,
|
||||
dockerfilePath: composeOpts.dockerfilePath,
|
||||
nogitignore: composeOpts.nogitignore,
|
||||
nogitignore: composeOpts.nogitignore, // v13: delete this line
|
||||
multiDockerignore: composeOpts.multiDockerignore,
|
||||
});
|
||||
}
|
||||
|
@ -34,8 +34,11 @@ export default class ConfigInjectCmd extends Command {
|
||||
public static description = stripIndent`
|
||||
Inject a configuration file into a device or OS image.
|
||||
|
||||
Inject a config.json file to the mounted filesystem,
|
||||
e.g. the SD card of a provisioned device or balenaOS image.
|
||||
Inject a config.json file to a mounted filesystem, e.g. the SD card of a
|
||||
provisioned device or balenaOS image.
|
||||
|
||||
Note: if using a private/custom device type, please ensure you are logged in
|
||||
('balena login' command). Public device types do not require logging in.
|
||||
`;
|
||||
|
||||
public static examples = [
|
||||
@ -59,8 +62,6 @@ export default class ConfigInjectCmd extends Command {
|
||||
help: cf.help,
|
||||
};
|
||||
|
||||
public static authenticated = true;
|
||||
|
||||
public static root = true;
|
||||
|
||||
public async run() {
|
||||
|
@ -34,7 +34,7 @@ import type {
|
||||
ComposeOpts,
|
||||
Release as ComposeReleaseInfo,
|
||||
} from '../utils/compose-types';
|
||||
import type { DockerCliFlags } from '../utils/docker';
|
||||
import type { BuildOpts, DockerCliFlags } from '../utils/docker';
|
||||
import {
|
||||
applyReleaseTagKeysAndValues,
|
||||
buildProject,
|
||||
@ -59,6 +59,7 @@ interface FlagsDef extends ComposeCliFlags, DockerCliFlags {
|
||||
build: boolean;
|
||||
nologupload: boolean;
|
||||
'release-tag'?: string[];
|
||||
draft: boolean;
|
||||
help: void;
|
||||
}
|
||||
|
||||
@ -136,6 +137,14 @@ ${dockerignoreHelp}
|
||||
`,
|
||||
multiple: true,
|
||||
}),
|
||||
draft: flags.boolean({
|
||||
description: stripIndent`
|
||||
Deploy the release as a draft. Draft releases are ignored
|
||||
by the 'track latest' release policy but can be used through release pinning.
|
||||
Draft releases can be marked as final through the API. Releases are created
|
||||
as final by default unless this option is given.`,
|
||||
default: false,
|
||||
}),
|
||||
...composeCliFlags,
|
||||
...dockerCliFlags,
|
||||
// NOTE: Not supporting -h for help, because of clash with -h in DockerCliFlags
|
||||
@ -213,6 +222,7 @@ ${dockerignoreHelp}
|
||||
shouldPerformBuild: !!options.build,
|
||||
shouldUploadLogs: !options.nologupload,
|
||||
buildEmulated: !!options.emulated,
|
||||
createAsDraft: options.draft,
|
||||
buildOpts,
|
||||
});
|
||||
await applyReleaseTagKeysAndValues(
|
||||
@ -235,7 +245,8 @@ ${dockerignoreHelp}
|
||||
shouldPerformBuild: boolean;
|
||||
shouldUploadLogs: boolean;
|
||||
buildEmulated: boolean;
|
||||
buildOpts: any; // arguments to forward to docker build command
|
||||
buildOpts: BuildOpts;
|
||||
createAsDraft: boolean;
|
||||
},
|
||||
) {
|
||||
const _ = await import('lodash');
|
||||
@ -248,7 +259,12 @@ ${dockerignoreHelp}
|
||||
const appType = (opts.app?.application_type as ApplicationType[])?.[0];
|
||||
|
||||
try {
|
||||
const project = await loadProject(logger, composeOpts, opts.image);
|
||||
const project = await loadProject(
|
||||
logger,
|
||||
composeOpts,
|
||||
opts.image,
|
||||
opts.buildOpts.t,
|
||||
);
|
||||
if (project.descriptors.length > 1 && !appType?.supports_multicontainer) {
|
||||
throw new ExpectedError(
|
||||
'Target fleet does not support multiple containers. Aborting!',
|
||||
@ -303,7 +319,7 @@ ${dockerignoreHelp}
|
||||
inlineLogs: composeOpts.inlineLogs,
|
||||
convertEol: composeOpts.convertEol,
|
||||
dockerfilePath: composeOpts.dockerfilePath,
|
||||
nogitignore: composeOpts.nogitignore,
|
||||
nogitignore: composeOpts.nogitignore, // v13: delete this line
|
||||
multiDockerignore: composeOpts.multiDockerignore,
|
||||
});
|
||||
builtImagesByService = _.keyBy(builtImages, 'serviceName');
|
||||
@ -367,6 +383,8 @@ ${dockerignoreHelp}
|
||||
`Bearer ${auth}`,
|
||||
apiEndpoint,
|
||||
!opts.shouldUploadLogs,
|
||||
composeOpts.projectPath,
|
||||
opts.createAsDraft,
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -17,7 +17,12 @@
|
||||
|
||||
import type { flags } from '@oclif/command';
|
||||
import type { IArg } from '@oclif/parser/lib/args';
|
||||
import type { Application, BalenaSDK } from 'balena-sdk';
|
||||
import type {
|
||||
BalenaSDK,
|
||||
Device,
|
||||
DeviceType,
|
||||
PineTypedResult,
|
||||
} from 'balena-sdk';
|
||||
import Command from '../../command';
|
||||
import * as cf from '../../utils/common-flags';
|
||||
import { ExpectedError } from '../../errors';
|
||||
@ -29,9 +34,12 @@ import {
|
||||
} from '../../utils/messages';
|
||||
import { isV13 } from '../../utils/version';
|
||||
|
||||
interface ExtendedDevice extends DeviceWithDeviceType {
|
||||
type ExtendedDevice = PineTypedResult<
|
||||
Device,
|
||||
typeof import('../../utils/helpers').expandForAppNameAndCpuArch
|
||||
> & {
|
||||
application_name?: string;
|
||||
}
|
||||
};
|
||||
|
||||
interface FlagsDef {
|
||||
application?: string;
|
||||
@ -94,7 +102,7 @@ export default class DeviceMoveCmd extends Command {
|
||||
const balena = getBalenaSdk();
|
||||
|
||||
const { tryAsInteger } = await import('../../utils/validation');
|
||||
const { expandForAppName } = await import('../../utils/helpers');
|
||||
const { expandForAppNameAndCpuArch } = await import('../../utils/helpers');
|
||||
|
||||
// Parse ids string into array of correct types
|
||||
const deviceIds: Array<string | number> = params.uuid
|
||||
@ -107,15 +115,14 @@ export default class DeviceMoveCmd extends Command {
|
||||
(uuid) =>
|
||||
balena.models.device.get(
|
||||
uuid,
|
||||
expandForAppName,
|
||||
expandForAppNameAndCpuArch,
|
||||
) as Promise<ExtendedDevice>,
|
||||
),
|
||||
);
|
||||
|
||||
// Map application name for each device
|
||||
for (const device of devices) {
|
||||
const belongsToApplication =
|
||||
device.belongs_to__application as Application[];
|
||||
const belongsToApplication = device.belongs_to__application;
|
||||
device.application_name = belongsToApplication?.[0]
|
||||
? belongsToApplication[0].app_name
|
||||
: 'N/a';
|
||||
@ -145,42 +152,56 @@ export default class DeviceMoveCmd extends Command {
|
||||
balena: BalenaSDK,
|
||||
devices: ExtendedDevice[],
|
||||
) {
|
||||
const [deviceDeviceTypes, deviceTypes] = await Promise.all([
|
||||
Promise.all(
|
||||
devices.map((device) =>
|
||||
balena.models.device.getManifestBySlug(
|
||||
device.is_of__device_type[0].slug,
|
||||
),
|
||||
const { getExpandedProp } = await import('../../utils/pine');
|
||||
// deduplicate the slugs
|
||||
const deviceCpuArchs = Array.from(
|
||||
new Set(
|
||||
devices.map(
|
||||
(d) => d.is_of__device_type[0].is_of__cpu_architecture[0].slug,
|
||||
),
|
||||
),
|
||||
balena.models.config.getDeviceTypes(),
|
||||
]);
|
||||
);
|
||||
|
||||
const compatibleDeviceTypes = deviceTypes.filter((dt) =>
|
||||
deviceDeviceTypes.every(
|
||||
(deviceDeviceType) =>
|
||||
balena.models.os.isArchitectureCompatibleWith(
|
||||
deviceDeviceType.arch,
|
||||
dt.arch,
|
||||
) &&
|
||||
!!dt.isDependent === !!deviceDeviceType.isDependent &&
|
||||
dt.state !== 'DISCONTINUED',
|
||||
),
|
||||
const deviceTypeOptions = {
|
||||
$select: 'slug',
|
||||
$expand: {
|
||||
is_of__cpu_architecture: {
|
||||
$select: 'slug',
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
const deviceTypes = (await balena.models.deviceType.getAllSupported(
|
||||
deviceTypeOptions,
|
||||
)) as Array<PineTypedResult<DeviceType, typeof deviceTypeOptions>>;
|
||||
|
||||
const compatibleDeviceTypeSlugs = new Set(
|
||||
deviceTypes
|
||||
.filter((deviceType) => {
|
||||
const deviceTypeArch = getExpandedProp(
|
||||
deviceType.is_of__cpu_architecture,
|
||||
'slug',
|
||||
)!;
|
||||
return deviceCpuArchs.every((deviceCpuArch) =>
|
||||
balena.models.os.isArchitectureCompatibleWith(
|
||||
deviceCpuArch,
|
||||
deviceTypeArch,
|
||||
),
|
||||
);
|
||||
})
|
||||
.map((deviceType) => deviceType.slug),
|
||||
);
|
||||
|
||||
const patterns = await import('../../utils/patterns');
|
||||
try {
|
||||
const application = await patterns.selectApplication(
|
||||
(app) =>
|
||||
compatibleDeviceTypes.some(
|
||||
(dt) => dt.slug === app.is_for__device_type[0].slug,
|
||||
) &&
|
||||
compatibleDeviceTypeSlugs.has(app.is_for__device_type[0].slug) &&
|
||||
devices.some((device) => device.application_name !== app.app_name),
|
||||
true,
|
||||
);
|
||||
return application;
|
||||
} catch (err) {
|
||||
if (deviceDeviceTypes.length) {
|
||||
if (!compatibleDeviceTypeSlugs.size) {
|
||||
throw new ExpectedError(
|
||||
`${err.message}\nDo all devices have a compatible architecture?`,
|
||||
);
|
||||
|
8
lib/commands/env/add.ts
vendored
8
lib/commands/env/add.ts
vendored
@ -227,7 +227,7 @@ async function setServiceVars(
|
||||
sdk,
|
||||
uuid,
|
||||
['id'],
|
||||
['app_name'],
|
||||
['slug'],
|
||||
);
|
||||
} catch (err) {
|
||||
console.error(`${err.message}, device: ${uuid}`);
|
||||
@ -236,11 +236,7 @@ async function setServiceVars(
|
||||
}
|
||||
for (const service of options.service!.split(',')) {
|
||||
try {
|
||||
const serviceId = await getServiceIdForApp(
|
||||
sdk,
|
||||
app.app_name,
|
||||
service,
|
||||
);
|
||||
const serviceId = await getServiceIdForApp(sdk, app.slug, service);
|
||||
await sdk.models.device.serviceVar.set(
|
||||
device.id,
|
||||
serviceId,
|
||||
|
2
lib/commands/env/rm.ts
vendored
2
lib/commands/env/rm.ts
vendored
@ -91,8 +91,6 @@ export default class EnvRmCmd extends Command {
|
||||
await confirm(
|
||||
opt.yes || false,
|
||||
'Are you sure you want to delete the environment variable?',
|
||||
undefined,
|
||||
true,
|
||||
);
|
||||
|
||||
const balena = getBalenaSdk();
|
||||
|
@ -174,11 +174,11 @@ export default class EnvsCmd extends Command {
|
||||
balena,
|
||||
options.device,
|
||||
['uuid'],
|
||||
['app_name'],
|
||||
['slug'],
|
||||
);
|
||||
fullUUID = device.uuid;
|
||||
if (app) {
|
||||
appNameOrSlug = app.app_name;
|
||||
appNameOrSlug = app.slug;
|
||||
}
|
||||
}
|
||||
if (appNameOrSlug && options.service) {
|
||||
@ -210,7 +210,14 @@ export default class EnvsCmd extends Command {
|
||||
|
||||
// Replace undefined app names with 'N/A' or null
|
||||
varArray = varArray.map((i: EnvironmentVariableInfo) => {
|
||||
i.appName = i.appName || (options.json ? null : 'N/A');
|
||||
if (i.appName) {
|
||||
// use slug in v13, app name in v12 for compatibility
|
||||
i.appName = isV13()
|
||||
? i.appName
|
||||
: i.appName.substring(i.appName.indexOf('/') + 1);
|
||||
} else {
|
||||
i.appName = options.json ? null : 'N/A';
|
||||
}
|
||||
return i;
|
||||
});
|
||||
|
||||
|
@ -20,12 +20,7 @@ import type { BlockDevice } from 'etcher-sdk/build/source-destination';
|
||||
import Command from '../../command';
|
||||
import { ExpectedError } from '../../errors';
|
||||
import * as cf from '../../utils/common-flags';
|
||||
import {
|
||||
getChalk,
|
||||
getCliForm,
|
||||
getVisuals,
|
||||
stripIndent,
|
||||
} from '../../utils/lazy';
|
||||
import { getChalk, getVisuals, stripIndent } from '../../utils/lazy';
|
||||
|
||||
interface FlagsDef {
|
||||
yes: boolean;
|
||||
@ -93,24 +88,15 @@ export default class LocalFlashCmd extends Command {
|
||||
}
|
||||
}
|
||||
|
||||
const { sourceDestination, multiWrite } = await import('etcher-sdk');
|
||||
|
||||
const drive = await this.getDrive(options);
|
||||
|
||||
const yes =
|
||||
options.yes ||
|
||||
(await getCliForm().ask({
|
||||
message: 'This will erase the selected drive. Are you sure?',
|
||||
type: 'confirm',
|
||||
name: 'yes',
|
||||
default: false,
|
||||
}));
|
||||
|
||||
if (!yes) {
|
||||
console.log(getChalk().red.bold('Aborted image flash'));
|
||||
process.exit(0);
|
||||
}
|
||||
const { confirm } = await import('../../utils/patterns');
|
||||
await confirm(
|
||||
options.yes,
|
||||
'This will erase the selected drive. Are you sure?',
|
||||
);
|
||||
|
||||
const { sourceDestination, multiWrite } = await import('etcher-sdk');
|
||||
const file = new sourceDestination.File({
|
||||
path: params.image,
|
||||
});
|
||||
|
@ -92,7 +92,6 @@ export default class OsInitializeCmd extends Command {
|
||||
options.yes,
|
||||
`This will erase ${answers.drive}. Are you sure?`,
|
||||
`Going to erase ${answers.drive}.`,
|
||||
true,
|
||||
);
|
||||
const { safeUmount } = await import('../../utils/umount');
|
||||
await safeUmount(answers.drive);
|
||||
|
@ -36,13 +36,7 @@ import { isV13 } from '../utils/version';
|
||||
|
||||
import { flags } from '@oclif/command';
|
||||
import * as _ from 'lodash';
|
||||
import type {
|
||||
Application,
|
||||
BalenaSDK,
|
||||
DeviceTypeJson,
|
||||
PineExpand,
|
||||
Release,
|
||||
} from 'balena-sdk';
|
||||
import type { Application, BalenaSDK, PineExpand, Release } from 'balena-sdk';
|
||||
import type { Preloader } from 'balena-preload';
|
||||
|
||||
interface FlagsDef extends DockerConnectionCliFlags {
|
||||
@ -331,7 +325,6 @@ Can be repeated to add multiple certificates.\
|
||||
readonly applicationExpandOptions: PineExpand<Application> = {
|
||||
owns__release: {
|
||||
$select: ['id', 'commit', 'end_timestamp', 'composition'],
|
||||
$orderby: [{ end_timestamp: 'desc' }, { id: 'desc' }],
|
||||
$expand: {
|
||||
contains__image: {
|
||||
$select: ['image'],
|
||||
@ -345,77 +338,75 @@ Can be repeated to add multiple certificates.\
|
||||
$filter: {
|
||||
status: 'success',
|
||||
},
|
||||
$orderby: [{ end_timestamp: 'desc' }, { id: 'desc' }],
|
||||
},
|
||||
should_be_running__release: {
|
||||
$select: 'commit',
|
||||
},
|
||||
};
|
||||
|
||||
allDeviceTypes: DeviceTypeJson.DeviceType[];
|
||||
async getDeviceTypes() {
|
||||
if (this.allDeviceTypes === undefined) {
|
||||
const balena = getBalenaSdk();
|
||||
const deviceTypes = await balena.models.config.getDeviceTypes();
|
||||
this.allDeviceTypes = _.sortBy(deviceTypes, 'name');
|
||||
}
|
||||
return this.allDeviceTypes;
|
||||
}
|
||||
|
||||
isCurrentCommit(commit: string) {
|
||||
return commit === 'latest' || commit === 'current';
|
||||
}
|
||||
|
||||
async getDeviceTypesWithSameArch(deviceTypeSlug: string) {
|
||||
const deviceTypes = await this.getDeviceTypes();
|
||||
const deviceType = _.find(deviceTypes, { slug: deviceTypeSlug });
|
||||
if (!deviceType) {
|
||||
throw new Error(`Device type "${deviceTypeSlug}" not found in API query`);
|
||||
}
|
||||
return _(deviceTypes).filter({ arch: deviceType.arch }).map('slug').value();
|
||||
}
|
||||
|
||||
async getApplicationsWithSuccessfulBuilds(deviceTypeSlug: string) {
|
||||
const balena = getBalenaSdk();
|
||||
|
||||
const deviceTypes = await this.getDeviceTypesWithSameArch(deviceTypeSlug);
|
||||
// TODO: remove the explicit types once https://github.com/balena-io/balena-sdk/pull/889 gets merged
|
||||
return balena.pine.get<
|
||||
Application,
|
||||
Array<
|
||||
ApplicationWithDeviceType & {
|
||||
should_be_running__release: [Release?];
|
||||
}
|
||||
>
|
||||
>({
|
||||
resource: 'my_application',
|
||||
options: {
|
||||
$filter: {
|
||||
is_for__device_type: {
|
||||
$any: {
|
||||
$alias: 'dt',
|
||||
$expr: {
|
||||
dt: {
|
||||
slug: { $in: deviceTypes },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
owns__release: {
|
||||
$any: {
|
||||
$alias: 'r',
|
||||
$expr: {
|
||||
r: {
|
||||
status: 'success',
|
||||
try {
|
||||
await balena.models.deviceType.get(deviceTypeSlug);
|
||||
} catch {
|
||||
throw new Error(`Device type "${deviceTypeSlug}" not found in API query`);
|
||||
}
|
||||
return (await balena.models.application.getAll({
|
||||
$select: ['id', 'app_name', 'should_track_latest_release'],
|
||||
$expand: this.applicationExpandOptions,
|
||||
$filter: {
|
||||
// get the apps that are of the same arch as the device type of the image
|
||||
is_for__device_type: {
|
||||
$any: {
|
||||
$alias: 'dt',
|
||||
$expr: {
|
||||
dt: {
|
||||
is_of__cpu_architecture: {
|
||||
$any: {
|
||||
$alias: 'ioca',
|
||||
$expr: {
|
||||
ioca: {
|
||||
is_supported_by__device_type: {
|
||||
$any: {
|
||||
$alias: 'isbdt',
|
||||
$expr: {
|
||||
isbdt: {
|
||||
slug: deviceTypeSlug,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
$expand: this.applicationExpandOptions,
|
||||
$select: ['id', 'app_name', 'should_track_latest_release'],
|
||||
$orderby: 'app_name asc',
|
||||
owns__release: {
|
||||
$any: {
|
||||
$alias: 'r',
|
||||
$expr: {
|
||||
r: {
|
||||
status: 'success',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
$orderby: 'app_name asc',
|
||||
})) as Array<
|
||||
ApplicationWithDeviceType & {
|
||||
should_be_running__release: [Release?];
|
||||
}
|
||||
>;
|
||||
}
|
||||
|
||||
async selectApplication(deviceTypeSlug: string) {
|
||||
|
@ -47,8 +47,8 @@ interface FlagsDef {
|
||||
pull: boolean;
|
||||
'noparent-check': boolean;
|
||||
'registry-secrets'?: string;
|
||||
gitignore?: boolean;
|
||||
nogitignore?: boolean;
|
||||
gitignore?: boolean; // v13: delete this flag
|
||||
nogitignore?: boolean; // v13: delete this flag
|
||||
nolive: boolean;
|
||||
detached: boolean;
|
||||
service?: string[];
|
||||
@ -58,6 +58,7 @@ interface FlagsDef {
|
||||
'noconvert-eol': boolean;
|
||||
'multi-dockerignore': boolean;
|
||||
'release-tag'?: string[];
|
||||
draft: boolean;
|
||||
help: void;
|
||||
}
|
||||
|
||||
@ -236,11 +237,20 @@ export default class PushCmd extends Command {
|
||||
'Have each service use its own .dockerignore file. See "balena help push".',
|
||||
char: 'm',
|
||||
default: false,
|
||||
exclusive: ['gitignore'],
|
||||
exclusive: ['gitignore'], // v13: delete this line
|
||||
}),
|
||||
...(isV13()
|
||||
? {}
|
||||
: {
|
||||
gitignore: flags.boolean({
|
||||
description: stripIndent`
|
||||
Consider .gitignore files in addition to the .dockerignore file. This reverts
|
||||
to the CLI v11 behavior/implementation (deprecated) if compatibility is
|
||||
required until your project can be adapted.`,
|
||||
char: 'g',
|
||||
default: false,
|
||||
exclusive: ['multi-dockerignore'],
|
||||
}),
|
||||
nogitignore: flags.boolean({
|
||||
description:
|
||||
'No-op (default behavior) since balena CLI v12.0.0. See "balena help push".',
|
||||
@ -249,15 +259,6 @@ export default class PushCmd extends Command {
|
||||
default: false,
|
||||
}),
|
||||
}),
|
||||
gitignore: flags.boolean({
|
||||
description: stripIndent`
|
||||
Consider .gitignore files in addition to the .dockerignore file. This reverts
|
||||
to the CLI v11 behavior/implementation (deprecated) if compatibility is
|
||||
required until your project can be adapted.`,
|
||||
char: 'g',
|
||||
default: false,
|
||||
exclusive: ['multi-dockerignore'],
|
||||
}),
|
||||
'release-tag': flags.string({
|
||||
description: stripIndent`
|
||||
Set release tags if the image build is successful (balenaCloud only). Multiple
|
||||
@ -267,6 +268,14 @@ export default class PushCmd extends Command {
|
||||
multiple: true,
|
||||
exclusive: ['detached'],
|
||||
}),
|
||||
draft: flags.boolean({
|
||||
description: stripIndent`
|
||||
Instruct the builder to create the release as a draft. Draft releases are ignored
|
||||
by the 'track latest' release policy but can be used through release pinning.
|
||||
Draft releases can be marked as final through the API. Releases are created
|
||||
as final by default unless this option is given.`,
|
||||
default: false,
|
||||
}),
|
||||
help: cf.help,
|
||||
};
|
||||
|
||||
@ -362,13 +371,14 @@ export default class PushCmd extends Command {
|
||||
registrySecrets,
|
||||
headless: options.detached,
|
||||
convertEol: !options['noconvert-eol'],
|
||||
isDraft: options.draft,
|
||||
};
|
||||
const args = {
|
||||
appSlug: application.slug,
|
||||
source: options.source,
|
||||
auth: token,
|
||||
baseUrl,
|
||||
nogitignore: !options.gitignore,
|
||||
nogitignore: !options.gitignore, // v13: delete this line
|
||||
sdk,
|
||||
opts,
|
||||
};
|
||||
@ -394,7 +404,7 @@ export default class PushCmd extends Command {
|
||||
registrySecrets: RegistrySecrets,
|
||||
) {
|
||||
// Check for invalid options
|
||||
const remoteOnlyOptions: Array<keyof FlagsDef> = ['release-tag'];
|
||||
const remoteOnlyOptions: Array<keyof FlagsDef> = ['release-tag', 'draft'];
|
||||
this.checkInvalidOptions(
|
||||
remoteOnlyOptions,
|
||||
options,
|
||||
@ -412,7 +422,7 @@ export default class PushCmd extends Command {
|
||||
multiDockerignore: options['multi-dockerignore'],
|
||||
nocache: options.nocache,
|
||||
pull: options.pull,
|
||||
nogitignore: !options.gitignore,
|
||||
nogitignore: !options.gitignore, // v13: delete this line
|
||||
noParentCheck: options['noparent-check'],
|
||||
nolive: options.nolive,
|
||||
detached: options.detached,
|
||||
|
86
lib/commands/release/finalize.ts
Normal file
86
lib/commands/release/finalize.ts
Normal file
@ -0,0 +1,86 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2016-2020 Balena Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { flags } from '@oclif/command';
|
||||
import Command from '../../command';
|
||||
import * as cf from '../../utils/common-flags';
|
||||
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
|
||||
import { tryAsInteger } from '../../utils/validation';
|
||||
|
||||
interface FlagsDef {
|
||||
help: void;
|
||||
}
|
||||
|
||||
interface ArgsDef {
|
||||
commitOrId: string | number;
|
||||
}
|
||||
|
||||
export default class ReleaseFinalizeCmd extends Command {
|
||||
public static description = stripIndent`
|
||||
Finalize a release.
|
||||
|
||||
Finalize a release. Releases can be "draft" or "final", and this command
|
||||
changes a draft release into a final release. Draft releases can be created
|
||||
with the \`--draft\` option of the \`balena build\` or \`balena deploy\`
|
||||
commands.
|
||||
|
||||
Draft releases are not automatically deployed to devices tracking the latest
|
||||
release. For a draft release to be deployed to a device, the device should be
|
||||
explicity pinned to that release. Conversely, final releases may trigger immediate
|
||||
deployment to unpinned devices (subject to a device's polling period) and, for
|
||||
this reason, final releases cannot be changed back to draft status.
|
||||
`;
|
||||
public static examples = [
|
||||
'$ balena release finalize a777f7345fe3d655c1c981aa642e5555',
|
||||
'$ balena release finalize 1234567',
|
||||
];
|
||||
|
||||
public static usage = 'release finalize <commitOrId>';
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
help: cf.help,
|
||||
};
|
||||
|
||||
public static args = [
|
||||
{
|
||||
name: 'commitOrId',
|
||||
description: 'the commit or ID of the release to finalize',
|
||||
required: true,
|
||||
parse: (commitOrId: string) => tryAsInteger(commitOrId),
|
||||
},
|
||||
];
|
||||
|
||||
public static authenticated = true;
|
||||
|
||||
public async run() {
|
||||
const { args: params } = this.parse<FlagsDef, ArgsDef>(ReleaseFinalizeCmd);
|
||||
|
||||
const balena = getBalenaSdk();
|
||||
|
||||
const release = await balena.models.release.get(params.commitOrId, {
|
||||
$select: ['id', 'is_final'],
|
||||
});
|
||||
|
||||
if (release.is_final) {
|
||||
console.log(`Release ${params.commitOrId} is already finalized!`);
|
||||
return;
|
||||
}
|
||||
|
||||
await balena.models.release.finalize(release.id);
|
||||
console.log(`Release ${params.commitOrId} finalized`);
|
||||
}
|
||||
}
|
128
lib/commands/release/index.ts
Normal file
128
lib/commands/release/index.ts
Normal file
@ -0,0 +1,128 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2016-2020 Balena Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { flags } from '@oclif/command';
|
||||
import Command from '../../command';
|
||||
import * as cf from '../../utils/common-flags';
|
||||
import { getBalenaSdk, getVisuals, stripIndent } from '../../utils/lazy';
|
||||
import type * as BalenaSdk from 'balena-sdk';
|
||||
import jsyaml = require('js-yaml');
|
||||
import { tryAsInteger } from '../../utils/validation';
|
||||
|
||||
interface FlagsDef {
|
||||
help: void;
|
||||
composition?: boolean;
|
||||
}
|
||||
|
||||
interface ArgsDef {
|
||||
commitOrId: string | number;
|
||||
}
|
||||
|
||||
export default class ReleaseCmd extends Command {
|
||||
public static description = stripIndent`
|
||||
Get info for a release.
|
||||
`;
|
||||
public static examples = [
|
||||
'$ balena release a777f7345fe3d655c1c981aa642e5555',
|
||||
'$ balena release 1234567',
|
||||
];
|
||||
|
||||
public static usage = 'release <commitOrId>';
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
help: cf.help,
|
||||
composition: flags.boolean({
|
||||
default: false,
|
||||
char: 'c',
|
||||
description: 'Return the release composition',
|
||||
}),
|
||||
};
|
||||
|
||||
public static args = [
|
||||
{
|
||||
name: 'commitOrId',
|
||||
description: 'the commit or ID of the release to get information',
|
||||
required: true,
|
||||
parse: (commitOrId: string) => tryAsInteger(commitOrId),
|
||||
},
|
||||
];
|
||||
|
||||
public static authenticated = true;
|
||||
|
||||
public async run() {
|
||||
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
|
||||
ReleaseCmd,
|
||||
);
|
||||
|
||||
const balena = getBalenaSdk();
|
||||
if (options.composition) {
|
||||
await this.showComposition(params.commitOrId, balena);
|
||||
} else {
|
||||
await this.showReleaseInfo(params.commitOrId, balena);
|
||||
}
|
||||
}
|
||||
|
||||
async showComposition(
|
||||
commitOrId: string | number,
|
||||
balena: BalenaSdk.BalenaSDK,
|
||||
) {
|
||||
const release = await balena.models.release.get(commitOrId, {
|
||||
$select: 'composition',
|
||||
});
|
||||
|
||||
console.log(jsyaml.dump(release.composition));
|
||||
}
|
||||
|
||||
async showReleaseInfo(
|
||||
commitOrId: string | number,
|
||||
balena: BalenaSdk.BalenaSDK,
|
||||
) {
|
||||
const fields: Array<keyof BalenaSdk.Release> = [
|
||||
'id',
|
||||
'commit',
|
||||
'created_at',
|
||||
'status',
|
||||
'semver',
|
||||
'is_final',
|
||||
'build_log',
|
||||
'start_timestamp',
|
||||
'end_timestamp',
|
||||
];
|
||||
|
||||
const release = await balena.models.release.get(commitOrId, {
|
||||
$select: fields,
|
||||
$expand: {
|
||||
release_tag: {
|
||||
$select: ['tag_key', 'value'],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const tagStr = release
|
||||
.release_tag!.map((t) => `${t.tag_key}=${t.value}`)
|
||||
.join('\n');
|
||||
|
||||
const _ = await import('lodash');
|
||||
const values = _.mapValues(
|
||||
release,
|
||||
(val) => val ?? 'N/a',
|
||||
) as Dictionary<string>;
|
||||
values['tags'] = tagStr;
|
||||
|
||||
console.log(getVisuals().table.vertical(values, [...fields, 'tags']));
|
||||
}
|
||||
}
|
86
lib/commands/releases.ts
Normal file
86
lib/commands/releases.ts
Normal file
@ -0,0 +1,86 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2016-2020 Balena Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { flags } from '@oclif/command';
|
||||
import Command from '../command';
|
||||
import * as cf from '../utils/common-flags';
|
||||
import { getBalenaSdk, getVisuals, stripIndent } from '../utils/lazy';
|
||||
import { applicationNameNote } from '../utils/messages';
|
||||
import type * as BalenaSdk from 'balena-sdk';
|
||||
|
||||
interface FlagsDef {
|
||||
help: void;
|
||||
}
|
||||
|
||||
interface ArgsDef {
|
||||
fleet: string;
|
||||
}
|
||||
|
||||
export default class ReleasesCmd extends Command {
|
||||
public static description = stripIndent`
|
||||
List all releases of a fleet.
|
||||
|
||||
List all releases of the given fleet.
|
||||
|
||||
${applicationNameNote.split('\n').join('\n\t\t')}
|
||||
`;
|
||||
public static examples = ['$ balena releases myorg/myfleet'];
|
||||
|
||||
public static usage = 'releases <fleet>';
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
help: cf.help,
|
||||
};
|
||||
|
||||
public static args = [
|
||||
{
|
||||
name: 'fleet',
|
||||
description: 'fleet name or slug',
|
||||
required: true,
|
||||
},
|
||||
];
|
||||
|
||||
public static authenticated = true;
|
||||
|
||||
public async run() {
|
||||
const { args: params } = this.parse<FlagsDef, ArgsDef>(ReleasesCmd);
|
||||
|
||||
const fields: Array<keyof BalenaSdk.Release> = [
|
||||
'id',
|
||||
'commit',
|
||||
'created_at',
|
||||
'status',
|
||||
'semver',
|
||||
'is_final',
|
||||
];
|
||||
|
||||
const balena = getBalenaSdk();
|
||||
|
||||
const releases = await balena.models.release.getAllByApplication(
|
||||
params.fleet,
|
||||
{ $select: fields },
|
||||
);
|
||||
|
||||
const _ = await import('lodash');
|
||||
console.log(
|
||||
getVisuals().table.horizontal(
|
||||
releases.map((rel) => _.mapValues(rel, (val) => val ?? 'N/a')),
|
||||
fields,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
230
lib/deprecation.ts
Normal file
230
lib/deprecation.ts
Normal file
@ -0,0 +1,230 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2021 Balena Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export interface ReleaseTimestampsByVersion {
|
||||
[version: string]: string; // e.g. { '12.0.0': '2021-06-16T12:54:52.000Z' }
|
||||
lastFetched: string; // ISO 8601 timestamp, e.g. '2021-06-27T16:46:10.000Z'
|
||||
}
|
||||
|
||||
/**
|
||||
* Warn about and enforce the CLI deprecation policy stated in the README
|
||||
* file. In particular:
|
||||
* The latest release of a major version will remain compatible with
|
||||
* the backend services for at least one year from the date when the
|
||||
* following major version is released. [...]
|
||||
* Half way through to that period (6 months), old major versions of the
|
||||
* balena CLI will start printing a deprecation warning message.
|
||||
* At the end of that period, older major versions will abort with an error
|
||||
* message unless the `--unsupported` flag is used.
|
||||
*
|
||||
* - Check for new balena-cli releases by querying the npm registry.
|
||||
* - Cache results for a number of days to improve performance.
|
||||
*
|
||||
* For this feature's specification and planning, see (restricted access):
|
||||
* https://jel.ly.fish/ed8d2395-9323-418c-bb67-d11d32a17d00
|
||||
*/
|
||||
export class DeprecationChecker {
|
||||
readonly majorVersionFetchIntervalDays = 7;
|
||||
readonly expiryDays = 365;
|
||||
readonly deprecationDays = Math.ceil(this.expiryDays / 2);
|
||||
readonly msInDay = 24 * 60 * 60 * 1000; // milliseconds in a day
|
||||
readonly debugPrefix = 'Deprecation check';
|
||||
readonly cacheFile = 'cachedReleaseTimestamps';
|
||||
readonly now = new Date().getTime();
|
||||
private initialized = false;
|
||||
storage: ReturnType<typeof import('balena-settings-storage')>;
|
||||
cachedTimestamps: ReleaseTimestampsByVersion;
|
||||
nextMajorVersion: string; // semver without the 'v' prefix
|
||||
|
||||
constructor(protected currentVersion: string) {
|
||||
const semver = require('semver') as typeof import('semver');
|
||||
const major = semver.major(this.currentVersion, { loose: true });
|
||||
this.nextMajorVersion = `${major + 1}.0.0`;
|
||||
}
|
||||
|
||||
public async init() {
|
||||
if (this.initialized) {
|
||||
return;
|
||||
}
|
||||
this.initialized = true;
|
||||
|
||||
const settings = await import('balena-settings-client');
|
||||
const getStorage = await import('balena-settings-storage');
|
||||
const dataDirectory = settings.get<string>('dataDirectory');
|
||||
this.storage = getStorage({ dataDirectory });
|
||||
let stored: ReleaseTimestampsByVersion | undefined;
|
||||
try {
|
||||
stored = (await this.storage.get(
|
||||
this.cacheFile,
|
||||
)) as ReleaseTimestampsByVersion;
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
this.cachedTimestamps = {
|
||||
...stored,
|
||||
// '1970-01-01T00:00:00.000Z' is new Date(0).toISOString()
|
||||
lastFetched: stored?.lastFetched || '1970-01-01T00:00:00.000Z',
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get NPM registry URL to retrieve the package.json file for a given version.
|
||||
* @param version Semver without 'v' prefix, e.g. '12.0.0.'
|
||||
*/
|
||||
protected getNpmUrl(version: string) {
|
||||
return `http://registry.npmjs.org/balena-cli/${version}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Query the npm registry (HTTP request) for a given balena-cli version.
|
||||
*
|
||||
* @param version semver version without the 'v' prefix, e.g. '13.0.0'
|
||||
* @returns `undefined` if the request status code is 404 (version not
|
||||
* published), otherwise a publishedAt date in ISO 8601 format, e.g.
|
||||
* '2021-06-27T16:46:10.000Z'.
|
||||
*/
|
||||
protected async fetchPublishedTimestampForVersion(
|
||||
version: string,
|
||||
): Promise<string | undefined> {
|
||||
const { default: got } = await import('got');
|
||||
const url = this.getNpmUrl(version);
|
||||
let response: import('got').Response<Dictionary<any>> | undefined;
|
||||
try {
|
||||
response = await got(url, { responseType: 'json', retry: 0 });
|
||||
} catch (e) {
|
||||
// 404 is expected if `version` hasn't been published yet
|
||||
if (e.response?.statusCode !== 404) {
|
||||
throw new Error(`Failed to query "${url}":\n${e}`);
|
||||
}
|
||||
}
|
||||
// response.body looks like a package.json file, plus possibly a
|
||||
// `versionist.publishedAt` field added by `github.com/product-os/versionist`
|
||||
const publishedAt: string | undefined =
|
||||
response?.body?.versionist?.publishedAt;
|
||||
if (!publishedAt && process.env.DEBUG) {
|
||||
console.error(`\
|
||||
[debug] ${this.debugPrefix}: balena CLI next major version "${this.nextMajorVersion}" not released, \
|
||||
or release date not available`);
|
||||
}
|
||||
return publishedAt; // ISO 8601, e.g. '2021-06-27T16:46:10.000Z'
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if we already know (cached value) when the next major version
|
||||
* was released. If we don't know, check how long ago the npm registry
|
||||
* was last fetched, and fetch again if it has been longer than
|
||||
* `majorVersionFetchIntervalDays`.
|
||||
*/
|
||||
public async checkForNewReleasesIfNeeded() {
|
||||
if (process.env.BALENARC_UNSUPPORTED) {
|
||||
return; // for the benefit of code testing
|
||||
}
|
||||
await this.init();
|
||||
if (this.cachedTimestamps[this.nextMajorVersion]) {
|
||||
// A cached value exists: no need to check the npm registry
|
||||
return;
|
||||
}
|
||||
const lastFetched = new Date(this.cachedTimestamps.lastFetched).getTime();
|
||||
const daysSinceLastFetch = (this.now - lastFetched) / this.msInDay;
|
||||
if (daysSinceLastFetch < this.majorVersionFetchIntervalDays) {
|
||||
if (process.env.DEBUG) {
|
||||
// toFixed(5) results in a precision of ~1 second
|
||||
const days = daysSinceLastFetch.toFixed(5);
|
||||
console.error(`\
|
||||
[debug] ${this.debugPrefix}: ${days} days since last npm registry query for next major version release date.
|
||||
[debug] Will not query the registry again until at least ${this.majorVersionFetchIntervalDays} days have passed.`);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (process.env.DEBUG) {
|
||||
console.error(`\
|
||||
[debug] ${
|
||||
this.debugPrefix
|
||||
}: Cache miss for the balena CLI next major version release date.
|
||||
[debug] Will query ${this.getNpmUrl(this.nextMajorVersion)}`);
|
||||
}
|
||||
try {
|
||||
const publishedAt = await this.fetchPublishedTimestampForVersion(
|
||||
this.nextMajorVersion,
|
||||
);
|
||||
if (publishedAt) {
|
||||
this.cachedTimestamps[this.nextMajorVersion] = publishedAt;
|
||||
}
|
||||
} catch (e) {
|
||||
if (process.env.DEBUG) {
|
||||
console.error(`[debug] ${this.debugPrefix}: ${e}`);
|
||||
}
|
||||
}
|
||||
// Refresh `lastFetched` regardless of whether or not the request to the npm
|
||||
// registry was successful. Will try again after `majorVersionFetchIntervalDays`.
|
||||
this.cachedTimestamps.lastFetched = new Date(this.now).toISOString();
|
||||
await this.storage.set(this.cacheFile, this.cachedTimestamps);
|
||||
}
|
||||
|
||||
/**
|
||||
* Use previously cached data (local cache only, fast execution) to check
|
||||
* whether this version of the CLI is deprecated as per deprecation policy,
|
||||
* in which case warn about it and conditionally throw an error.
|
||||
*/
|
||||
public async warnAndAbortIfDeprecated() {
|
||||
if (process.env.BALENARC_UNSUPPORTED) {
|
||||
return; // for the benefit of code testing
|
||||
}
|
||||
await this.init();
|
||||
const nextMajorDateStr = this.cachedTimestamps[this.nextMajorVersion];
|
||||
if (!nextMajorDateStr) {
|
||||
return;
|
||||
}
|
||||
const nextMajorDate = new Date(nextMajorDateStr).getTime();
|
||||
const daysElapsed = Math.trunc((this.now - nextMajorDate) / this.msInDay);
|
||||
if (daysElapsed > this.expiryDays) {
|
||||
const { ExpectedError } = await import('./errors');
|
||||
throw new ExpectedError(this.getExpiryMsg(daysElapsed));
|
||||
} else if (daysElapsed > this.deprecationDays && process.stderr.isTTY) {
|
||||
console.error(this.getDeprecationMsg(daysElapsed));
|
||||
}
|
||||
}
|
||||
|
||||
/** Separate function for the benefit of code testing */
|
||||
getDeprecationMsg(daysElapsed: number) {
|
||||
const { warnify } =
|
||||
require('./utils/messages') as typeof import('./utils/messages');
|
||||
return warnify(`\
|
||||
CLI version ${this.nextMajorVersion} was released ${daysElapsed} days ago: please upgrade.
|
||||
This version of the balena CLI (${this.currentVersion}) will exit with an error
|
||||
message after ${this.expiryDays} days from the release of version ${this.nextMajorVersion},
|
||||
as per deprecation policy: https://git.io/JRHUW#deprecation-policy
|
||||
|
||||
The --unsupported flag may be used to bypass this deprecation check and
|
||||
allow the CLI to keep working beyond the deprecation period. However,
|
||||
note that the balenaCloud or openBalena backends may be updated in a way
|
||||
that is no longer compatible with this version.`);
|
||||
}
|
||||
|
||||
/** Separate function the benefit of code testing */
|
||||
getExpiryMsg(daysElapsed: number) {
|
||||
return `
|
||||
This version of the balena CLI (${this.currentVersion}) has expired: please upgrade.
|
||||
${daysElapsed} days have passed since the release of CLI version ${this.nextMajorVersion}.
|
||||
See deprecation policy at: https://git.io/JRHUW#deprecation-policy
|
||||
|
||||
The --unsupported flag may be used to bypass this deprecation check and
|
||||
continue using this version of the CLI. However, note that the balenaCloud
|
||||
or openBalena backends may be updated in a way that is no longer compatible
|
||||
with this CLI version.`;
|
||||
}
|
||||
}
|
@ -286,24 +286,3 @@ export const printErrorMessage = function (message: string) {
|
||||
export const printExpectedErrorMessage = function (message: string) {
|
||||
console.error(`${message}\n`);
|
||||
};
|
||||
|
||||
/**
|
||||
* Print a friendly error message and exit the CLI with an error code, BYPASSING
|
||||
* error reporting through Sentry.io's platform (raven.Raven.captureException).
|
||||
* Note that lib/errors.ts provides top-level error handling code to catch any
|
||||
* otherwise uncaught errors, AND to report them through Sentry.io. But many
|
||||
* "expected" errors (say, a JSON parsing error in a file provided by the user)
|
||||
* don't warrant reporting through Sentry.io. For such mundane errors, catch
|
||||
* them and call this function.
|
||||
*
|
||||
* DEPRECATED: Use `throw new ExpectedError(<message>)` instead.
|
||||
* If a specific process exit code x must be set, use process.exitCode = x
|
||||
*/
|
||||
export function exitWithExpectedError(message: string | Error): never {
|
||||
if (message instanceof Error) {
|
||||
({ message } = message);
|
||||
}
|
||||
|
||||
printErrorMessage(message);
|
||||
process.exit(1);
|
||||
}
|
||||
|
@ -14,27 +14,17 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import * as _ from 'lodash';
|
||||
import * as Mixpanel from 'mixpanel';
|
||||
|
||||
import * as packageJSON from '../package.json';
|
||||
import { getBalenaSdk } from './utils/lazy';
|
||||
|
||||
const getMixpanel = _.once((balenaUrl: string) => {
|
||||
return Mixpanel.init('balena-main', {
|
||||
host: `api.${balenaUrl}`,
|
||||
path: '/mixpanel',
|
||||
protocol: 'https',
|
||||
});
|
||||
});
|
||||
|
||||
interface CachedUsername {
|
||||
token: string;
|
||||
username: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mixpanel.com analytics tracking (information on balena CLI usage).
|
||||
* Track balena CLI usage events (product improvement analytics).
|
||||
*
|
||||
* @param commandSignature A string like, for example:
|
||||
* "push <fleetOrDevice>"
|
||||
@ -60,11 +50,10 @@ export async function trackCommand(commandSignature: string) {
|
||||
});
|
||||
}
|
||||
const settings = await import('balena-settings-client');
|
||||
const balenaUrl = settings.get('balenaUrl') as string;
|
||||
|
||||
const username = await (async () => {
|
||||
const getStorage = await import('balena-settings-storage');
|
||||
const dataDirectory = settings.get('dataDirectory') as string;
|
||||
const dataDirectory = settings.get<string>('dataDirectory');
|
||||
const storage = getStorage({ dataDirectory });
|
||||
let token;
|
||||
try {
|
||||
@ -94,8 +83,6 @@ export async function trackCommand(commandSignature: string) {
|
||||
}
|
||||
})();
|
||||
|
||||
const mixpanel = getMixpanel(balenaUrl);
|
||||
|
||||
if (!process.env.BALENARC_NO_SENTRY) {
|
||||
Sentry!.configureScope((scope) => {
|
||||
scope.setUser({
|
||||
@ -109,16 +96,43 @@ export async function trackCommand(commandSignature: string) {
|
||||
!process.env.BALENA_CLI_TEST_TYPE &&
|
||||
!process.env.BALENARC_NO_ANALYTICS
|
||||
) {
|
||||
await mixpanel.track(`[CLI] ${commandSignature}`, {
|
||||
distinct_id: username,
|
||||
version: packageJSON.version,
|
||||
node: process.version,
|
||||
arch: process.arch,
|
||||
balenaUrl, // e.g. 'balena-cloud.com' or 'balena-staging.com'
|
||||
platform: process.platform,
|
||||
});
|
||||
const balenaUrl = settings.get<string>('balenaUrl');
|
||||
await sendEvent(balenaUrl, `[CLI] ${commandSignature}`, username);
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Make the event tracking HTTPS request to balenaCloud's '/mixpanel' endpoint.
|
||||
*/
|
||||
async function sendEvent(balenaUrl: string, event: string, username?: string) {
|
||||
const { default: got } = await import('got');
|
||||
const trackData = {
|
||||
event,
|
||||
properties: {
|
||||
arch: process.arch,
|
||||
balenaUrl, // e.g. 'balena-cloud.com' or 'balena-staging.com'
|
||||
distinct_id: username,
|
||||
mp_lib: 'node',
|
||||
node: process.version,
|
||||
platform: process.platform,
|
||||
token: 'balena-main',
|
||||
version: packageJSON.version,
|
||||
},
|
||||
};
|
||||
const url = `https://api.${balenaUrl}/mixpanel/track`;
|
||||
const searchParams = {
|
||||
ip: 0,
|
||||
verbose: 0,
|
||||
data: Buffer.from(JSON.stringify(trackData)).toString('base64'),
|
||||
};
|
||||
try {
|
||||
await got(url, { searchParams, retry: 0 });
|
||||
} catch (e) {
|
||||
if (process.env.DEBUG) {
|
||||
console.error(`[debug] Event tracking error: ${e.message || e}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
139
lib/help.ts
139
lib/help.ts
@ -46,7 +46,7 @@ export default class BalenaHelp extends Help {
|
||||
const subject = getHelpSubject(argv);
|
||||
if (!subject) {
|
||||
const verbose = argv.includes('-v') || argv.includes('--verbose');
|
||||
this.showCustomRootHelp(verbose);
|
||||
console.log(this.getCustomRootHelp(verbose));
|
||||
return;
|
||||
}
|
||||
|
||||
@ -80,67 +80,106 @@ export default class BalenaHelp extends Help {
|
||||
throw new ExpectedError(`command ${chalk.cyan.bold(subject)} not found`);
|
||||
}
|
||||
|
||||
showCustomRootHelp(showAllCommands: boolean): void {
|
||||
const chalk = getChalk();
|
||||
const bold = chalk.bold;
|
||||
const cmd = chalk.cyan.bold;
|
||||
getCustomRootHelp(showAllCommands: boolean): string {
|
||||
const { bold, cyan } = getChalk();
|
||||
|
||||
let commands = this.config.commands;
|
||||
commands = commands.filter((c) => this.opts.all || !c.hidden);
|
||||
|
||||
// Get Primary Commands, sorted as in manual list
|
||||
const primaryCommands = this.manuallySortedPrimaryCommands.map((pc) => {
|
||||
return commands.find((c) => c.id === pc.replace(' ', ':'));
|
||||
});
|
||||
const primaryCommands = this.manuallySortedPrimaryCommands
|
||||
.map((pc) => {
|
||||
return commands.find((c) => c.id === pc.replace(' ', ':'));
|
||||
})
|
||||
.filter((c): c is typeof commands[0] => !!c);
|
||||
|
||||
// Get the rest as Additional Commands
|
||||
const additionalCommands = commands.filter(
|
||||
(c) =>
|
||||
!this.manuallySortedPrimaryCommands.includes(c.id.replace(':', ' ')),
|
||||
);
|
||||
|
||||
// Find longest usage, and pad usage of first command in each category
|
||||
// This is to ensure that both categories align visually
|
||||
const usageLength = commands
|
||||
.map((c) => c.usage?.length || 0)
|
||||
.reduce((longest, l) => {
|
||||
return l > longest ? l : longest;
|
||||
});
|
||||
|
||||
if (
|
||||
typeof primaryCommands[0]?.usage === 'string' &&
|
||||
typeof additionalCommands[0]?.usage === 'string'
|
||||
) {
|
||||
primaryCommands[0].usage = primaryCommands[0].usage.padEnd(usageLength);
|
||||
additionalCommands[0].usage =
|
||||
additionalCommands[0].usage.padEnd(usageLength);
|
||||
let usageLength = 0;
|
||||
for (const cmd of primaryCommands) {
|
||||
usageLength = Math.max(usageLength, cmd.usage?.length || 0);
|
||||
}
|
||||
|
||||
// Output help
|
||||
console.log(bold('USAGE'));
|
||||
console.log('$ balena [COMMAND] [OPTIONS]');
|
||||
|
||||
console.log(bold('\nPRIMARY COMMANDS'));
|
||||
console.log(this.formatCommands(primaryCommands));
|
||||
|
||||
let additionalCmdSection: string[];
|
||||
if (showAllCommands) {
|
||||
console.log(bold('\nADDITIONAL COMMANDS'));
|
||||
console.log(this.formatCommands(additionalCommands));
|
||||
// Get the rest as Additional Commands
|
||||
const additionalCommands = commands.filter(
|
||||
(c) =>
|
||||
!this.manuallySortedPrimaryCommands.includes(c.id.replace(':', ' ')),
|
||||
);
|
||||
|
||||
// Find longest usage, and pad usage of first command in each category
|
||||
// This is to ensure that both categories align visually
|
||||
for (const cmd of additionalCommands) {
|
||||
usageLength = Math.max(usageLength, cmd.usage?.length || 0);
|
||||
}
|
||||
|
||||
if (
|
||||
typeof primaryCommands[0].usage === 'string' &&
|
||||
typeof additionalCommands[0].usage === 'string'
|
||||
) {
|
||||
primaryCommands[0].usage = primaryCommands[0].usage.padEnd(usageLength);
|
||||
additionalCommands[0].usage =
|
||||
additionalCommands[0].usage.padEnd(usageLength);
|
||||
}
|
||||
|
||||
additionalCmdSection = [
|
||||
bold('\nADDITIONAL COMMANDS'),
|
||||
this.formatCommands(additionalCommands),
|
||||
];
|
||||
} else {
|
||||
console.log(
|
||||
`\n${bold('...MORE')} run ${cmd(
|
||||
'balena help --verbose',
|
||||
)} to list additional commands.`,
|
||||
const cmd = cyan.bold('balena help --verbose');
|
||||
additionalCmdSection = [
|
||||
`\n${bold('...MORE')} run ${cmd} to list additional commands.`,
|
||||
];
|
||||
}
|
||||
|
||||
const globalOps = [
|
||||
['--help, -h', 'display command help'],
|
||||
['--debug', 'enable debug output'],
|
||||
[
|
||||
'--unsupported',
|
||||
`\
|
||||
prevent exit with an error as per Deprecation Policy
|
||||
See: https://git.io/JRHUW#deprecation-policy`,
|
||||
],
|
||||
];
|
||||
globalOps[0][0] = globalOps[0][0].padEnd(usageLength);
|
||||
|
||||
const { deprecationPolicyNote, reachingOut } =
|
||||
require('./utils/messages') as typeof import('./utils/messages');
|
||||
|
||||
return [
|
||||
bold('USAGE'),
|
||||
'$ balena [COMMAND] [OPTIONS]',
|
||||
bold('\nPRIMARY COMMANDS'),
|
||||
this.formatCommands(primaryCommands),
|
||||
...additionalCmdSection,
|
||||
bold('\nGLOBAL OPTIONS'),
|
||||
this.formatGlobalOpts(globalOps),
|
||||
bold('\nDeprecation Policy Reminder'),
|
||||
deprecationPolicyNote,
|
||||
reachingOut,
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
protected formatGlobalOpts(opts: string[][]) {
|
||||
const { dim } = getChalk();
|
||||
const outLines: string[] = [];
|
||||
let flagWidth = 0;
|
||||
for (const opt of opts) {
|
||||
flagWidth = Math.max(flagWidth, opt[0].length);
|
||||
}
|
||||
for (const opt of opts) {
|
||||
const descriptionLines = opt[1].split('\n');
|
||||
outLines.push(
|
||||
` ${opt[0].padEnd(flagWidth + 2)}${dim(descriptionLines[0])}`,
|
||||
);
|
||||
outLines.push(
|
||||
...descriptionLines
|
||||
.slice(1)
|
||||
.map((line) => ` ${' '.repeat(flagWidth + 2)}${dim(line)}`),
|
||||
);
|
||||
}
|
||||
|
||||
console.log(bold('\nGLOBAL OPTIONS'));
|
||||
console.log(' --help, -h');
|
||||
console.log(' --debug\n');
|
||||
|
||||
const { reachingOut } =
|
||||
require('./utils/messages') as typeof import('./utils/messages');
|
||||
console.log(reachingOut);
|
||||
return outLines.join('\n');
|
||||
}
|
||||
|
||||
protected formatCommands(commands: any[]): string {
|
||||
|
@ -28,7 +28,7 @@ export const trackPromise = new Promise((resolve) => {
|
||||
* parsed by oclif, but before the command's run() function is called.
|
||||
* See: https://oclif.io/docs/hooks
|
||||
*
|
||||
* This hook is used to track CLI command signatures with mixpanel.
|
||||
* This hook is used to track CLI command signatures (usage analytics).
|
||||
* A command signature is something like "env add NAME [VALUE]". That's
|
||||
* literally so: 'NAME' and 'VALUE' are NOT replaced with actual values.
|
||||
*/
|
||||
|
@ -14,8 +14,8 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { stripIndent } from './utils/lazy';
|
||||
import { exitWithExpectedError } from './errors';
|
||||
|
||||
export let unsupportedFlag = false;
|
||||
|
||||
export interface AppOptions {
|
||||
// Prevent the default behavior of flushing stdout after running a command
|
||||
@ -50,11 +50,10 @@ export async function preparseArgs(argv: string[]): Promise<string[]> {
|
||||
}
|
||||
|
||||
// support global --debug flag
|
||||
const debugIndex = cmdSlice.indexOf('--debug');
|
||||
if (debugIndex > -1) {
|
||||
if (extractBooleanFlag(cmdSlice, '--debug')) {
|
||||
process.env.DEBUG = '1';
|
||||
cmdSlice.splice(debugIndex, 1);
|
||||
}
|
||||
unsupportedFlag = extractBooleanFlag(cmdSlice, '--unsupported');
|
||||
}
|
||||
|
||||
// Enable bluebird long stack traces when in debug mode, must be set
|
||||
@ -87,11 +86,22 @@ export async function preparseArgs(argv: string[]): Promise<string[]> {
|
||||
return args;
|
||||
}
|
||||
|
||||
function extractBooleanFlag(argv: string[], flag: string): boolean {
|
||||
const index = argv.indexOf(flag);
|
||||
if (index >= 0) {
|
||||
argv.splice(index, 1);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether the command line refers to a command that has been deprecated
|
||||
* and removed and, if so, exit with an informative error message.
|
||||
*/
|
||||
export function checkDeletedCommand(argvSlice: string[]): void {
|
||||
const { ExpectedError } = require('./errors') as typeof import('./errors');
|
||||
|
||||
if (argvSlice[0] === 'help') {
|
||||
argvSlice = argvSlice.slice(1);
|
||||
}
|
||||
@ -101,17 +111,16 @@ export function checkDeletedCommand(argvSlice: string[]): void {
|
||||
version: string,
|
||||
verb = 'replaced',
|
||||
) {
|
||||
exitWithExpectedError(stripIndent`
|
||||
Note: the command "balena ${oldCmd}" was ${verb} in CLI version ${version}.
|
||||
Please use "balena ${alternative}" instead.
|
||||
`);
|
||||
throw new ExpectedError(`\
|
||||
Note: the command "balena ${oldCmd}" was ${verb} in CLI version ${version}.
|
||||
Please use "balena ${alternative}" instead.`);
|
||||
}
|
||||
function removed(oldCmd: string, alternative: string, version: string) {
|
||||
let msg = `Note: the command "balena ${oldCmd}" was removed in CLI version ${version}.`;
|
||||
if (alternative) {
|
||||
msg = [msg, alternative].join('\n');
|
||||
}
|
||||
exitWithExpectedError(msg);
|
||||
throw new ExpectedError(msg);
|
||||
}
|
||||
const stopAlternative =
|
||||
'Please use "balena ssh -s" to access the host OS, then use `balena-engine stop`.';
|
||||
|
@ -139,6 +139,14 @@ export async function downloadOSImage(
|
||||
|
||||
const displayVersion = OSVersion === 'default' ? '' : ` ${OSVersion}`;
|
||||
|
||||
// Override the default zlib flush value as we've seen cases of
|
||||
// incomplete files being identified as successful downloads when using Z_SYNC_FLUSH.
|
||||
// Using Z_NO_FLUSH results in a Z_BUF_ERROR instead of a corrupt image file.
|
||||
// https://github.com/nodejs/node/blob/master/doc/api/zlib.md#zlib-constants
|
||||
// Hopefully this is a temporary workaround until we can resolve
|
||||
// some ongoing issues with the os download stream.
|
||||
process.env.ZLIB_FLUSH = 'Z_NO_FLUSH';
|
||||
|
||||
const manager = await import('balena-image-manager');
|
||||
const stream = await manager.get(deviceType, OSVersion);
|
||||
|
||||
@ -188,18 +196,21 @@ async function resolveOSVersion(deviceType: string, version: string) {
|
||||
return version;
|
||||
}
|
||||
|
||||
const { versions: vs, recommended } =
|
||||
await getBalenaSdk().models.os.getSupportedVersions(deviceType);
|
||||
const vs = (
|
||||
(await getBalenaSdk().models.hostapp.getAllOsVersions([deviceType]))[
|
||||
deviceType
|
||||
] ?? []
|
||||
).filter((v) => v.osType === 'default');
|
||||
|
||||
const choices = vs.map((v) => ({
|
||||
value: v,
|
||||
name: `v${v}` + (v === recommended ? ' (recommended)' : ''),
|
||||
value: v.rawVersion,
|
||||
name: `v${v.rawVersion}` + (v.isRecommended ? ' (recommended)' : ''),
|
||||
}));
|
||||
|
||||
return getCliForm().ask({
|
||||
message: 'Select the OS version:',
|
||||
type: 'list',
|
||||
choices,
|
||||
default: recommended,
|
||||
default: (vs.find((v) => v.isRecommended) ?? vs[0])?.rawVersion,
|
||||
});
|
||||
}
|
||||
|
11
lib/utils/compose-types.d.ts
vendored
11
lib/utils/compose-types.d.ts
vendored
@ -51,7 +51,7 @@ export interface ComposeOpts {
|
||||
dockerfilePath?: string;
|
||||
inlineLogs?: boolean;
|
||||
multiDockerignore: boolean;
|
||||
nogitignore: boolean;
|
||||
nogitignore: boolean; // v13: delete this line
|
||||
noParentCheck: boolean;
|
||||
projectName: string;
|
||||
projectPath: string;
|
||||
@ -63,9 +63,9 @@ export interface ComposeCliFlags {
|
||||
dockerfile?: string;
|
||||
logs: boolean;
|
||||
nologs: boolean;
|
||||
gitignore: boolean;
|
||||
gitignore?: boolean; // v13: delete this line
|
||||
nogitignore?: boolean; // v13: delete this line
|
||||
'multi-dockerignore': boolean;
|
||||
nogitignore: boolean;
|
||||
'noparent-check': boolean;
|
||||
'registry-secrets'?: RegistrySecrets;
|
||||
'convert-eol': boolean;
|
||||
@ -89,6 +89,9 @@ export interface Release {
|
||||
| 'commit'
|
||||
| 'composition'
|
||||
| 'source'
|
||||
| 'is_final'
|
||||
| 'contract'
|
||||
| 'semver'
|
||||
| 'start_timestamp'
|
||||
| 'end_timestamp'
|
||||
>;
|
||||
@ -99,6 +102,6 @@ interface TarDirectoryOptions {
|
||||
composition?: Composition;
|
||||
convertEol?: boolean;
|
||||
multiDockerignore?: boolean;
|
||||
nogitignore: boolean;
|
||||
nogitignore: boolean; // v13: delete this line
|
||||
preFinalizeCallback?: (pack: Pack) => void | Promise<void>;
|
||||
}
|
||||
|
@ -18,6 +18,7 @@
|
||||
import * as path from 'path';
|
||||
import { ExpectedError } from '../errors';
|
||||
import { getChalk } from './lazy';
|
||||
import { isV13 } from './version';
|
||||
|
||||
/**
|
||||
* @returns Promise<{import('./compose-types').ComposeOpts}>
|
||||
@ -25,7 +26,7 @@ import { getChalk } from './lazy';
|
||||
export function generateOpts(options) {
|
||||
const { promises: fs } = require('fs');
|
||||
|
||||
if (options.gitignore && options['multi-dockerignore']) {
|
||||
if (!isV13() && options.gitignore && options['multi-dockerignore']) {
|
||||
throw new ExpectedError(
|
||||
'The --gitignore and --multi-dockerignore options cannot be used together',
|
||||
);
|
||||
@ -37,7 +38,7 @@ export function generateOpts(options) {
|
||||
convertEol: !options['noconvert-eol'],
|
||||
dockerfilePath: options.dockerfile,
|
||||
multiDockerignore: !!options['multi-dockerignore'],
|
||||
nogitignore: !options.gitignore,
|
||||
nogitignore: !options.gitignore, // v13: delete this line
|
||||
noParentCheck: options['noparent-check'],
|
||||
}));
|
||||
}
|
||||
@ -48,10 +49,16 @@ export function generateOpts(options) {
|
||||
/**
|
||||
* @param {string} composePath
|
||||
* @param {string} composeStr
|
||||
* @param {string | null} projectName
|
||||
* @param {string | undefined} projectName The --projectName flag (build, deploy)
|
||||
* @param {string | undefined} imageTag The --tag flag (build, deploy)
|
||||
* @returns {import('./compose-types').ComposeProject}
|
||||
*/
|
||||
export function createProject(composePath, composeStr, projectName = null) {
|
||||
export function createProject(
|
||||
composePath,
|
||||
composeStr,
|
||||
projectName = '',
|
||||
imageTag = '',
|
||||
) {
|
||||
const yml = require('js-yaml');
|
||||
const compose = require('resin-compose-parse');
|
||||
|
||||
@ -61,7 +68,7 @@ export function createProject(composePath, composeStr, projectName = null) {
|
||||
});
|
||||
const composition = compose.normalize(rawComposition);
|
||||
|
||||
projectName ??= path.basename(composePath);
|
||||
projectName ||= path.basename(composePath);
|
||||
|
||||
const descriptors = compose.parse(composition).map(function (descr) {
|
||||
// generate an image name based on the project and service names
|
||||
@ -71,9 +78,8 @@ export function createProject(composePath, composeStr, projectName = null) {
|
||||
descr.image.context != null &&
|
||||
descr.image.tag == null
|
||||
) {
|
||||
descr.image.tag = [projectName, descr.serviceName]
|
||||
.join('_')
|
||||
.toLowerCase();
|
||||
const { makeImageName } = require('./compose_ts');
|
||||
descr.image.tag = makeImageName(projectName, descr.serviceName, imageTag);
|
||||
}
|
||||
return descr;
|
||||
});
|
||||
@ -92,6 +98,8 @@ export function createProject(composePath, composeStr, projectName = null) {
|
||||
* @param {string} dir Source directory
|
||||
* @param {import('./compose-types').TarDirectoryOptions} param
|
||||
* @returns {Promise<import('stream').Readable>}
|
||||
*
|
||||
* v13: delete this function
|
||||
*/
|
||||
export async function originalTarDirectory(dir, param) {
|
||||
let {
|
||||
@ -175,6 +183,9 @@ export async function originalTarDirectory(dir, param) {
|
||||
* @param {number} userId
|
||||
* @param {number} appId
|
||||
* @param {import('resin-compose-parse').Composition} composition
|
||||
* @param {boolean} draft
|
||||
* @param {string|undefined} semver
|
||||
* @param {string|undefined} contract
|
||||
* @returns {Promise<import('./compose-types').Release>}
|
||||
*/
|
||||
export const createRelease = async function (
|
||||
@ -183,6 +194,9 @@ export const createRelease = async function (
|
||||
userId,
|
||||
appId,
|
||||
composition,
|
||||
draft,
|
||||
semver,
|
||||
contract,
|
||||
) {
|
||||
const _ = require('lodash');
|
||||
const crypto = require('crypto');
|
||||
@ -197,6 +211,9 @@ export const createRelease = async function (
|
||||
composition,
|
||||
source: 'local',
|
||||
commit: crypto.pseudoRandomBytes(16).toString('hex').toLowerCase(),
|
||||
semver,
|
||||
is_final: !draft,
|
||||
contract,
|
||||
});
|
||||
|
||||
return {
|
||||
@ -207,6 +224,9 @@ export const createRelease = async function (
|
||||
'commit',
|
||||
'composition',
|
||||
'source',
|
||||
'is_final',
|
||||
'contract',
|
||||
'semver',
|
||||
'start_timestamp',
|
||||
'end_timestamp',
|
||||
]),
|
||||
@ -435,7 +455,6 @@ var pushProgressRenderer = function (tty, prefix) {
|
||||
export class BuildProgressUI {
|
||||
constructor(tty, descriptors) {
|
||||
this._handleEvent = this._handleEvent.bind(this);
|
||||
this._handleInterrupt = this._handleInterrupt.bind(this);
|
||||
this.start = this.start.bind(this);
|
||||
this.end = this.end.bind(this);
|
||||
this._display = this._display.bind(this);
|
||||
@ -487,14 +506,7 @@ export class BuildProgressUI {
|
||||
this._serviceToDataMap[service] = event;
|
||||
}
|
||||
|
||||
_handleInterrupt() {
|
||||
this._cancelled = true;
|
||||
this.end();
|
||||
return process.exit(130); // 128 + SIGINT
|
||||
}
|
||||
|
||||
start() {
|
||||
process.on('SIGINT', this._handleInterrupt);
|
||||
this._tty.hideCursor();
|
||||
this._services.forEach((service) => {
|
||||
this.streams[service].write({ status: 'Preparing...' });
|
||||
@ -508,7 +520,6 @@ export class BuildProgressUI {
|
||||
return;
|
||||
}
|
||||
this._ended = true;
|
||||
process.removeListener('SIGINT', this._handleInterrupt);
|
||||
this._runloop?.end();
|
||||
this._runloop = null;
|
||||
|
||||
|
@ -18,8 +18,9 @@ import { flags } from '@oclif/command';
|
||||
import { BalenaSDK } from 'balena-sdk';
|
||||
import type { TransposeOptions } from 'docker-qemu-transpose';
|
||||
import type * as Dockerode from 'dockerode';
|
||||
import * as _ from 'lodash';
|
||||
import { promises as fs } from 'fs';
|
||||
import jsyaml = require('js-yaml');
|
||||
import * as _ from 'lodash';
|
||||
import * as path from 'path';
|
||||
import type {
|
||||
BuildConfig,
|
||||
@ -42,8 +43,11 @@ import {
|
||||
import type { DeviceInfo } from './device/api';
|
||||
import { getBalenaSdk, getChalk, stripIndent } from './lazy';
|
||||
import Logger = require('./logger');
|
||||
import { isV13 } from './version';
|
||||
import { exists } from './which';
|
||||
|
||||
const allowedContractTypes = ['sw.application', 'sw.block'];
|
||||
|
||||
/**
|
||||
* Given an array representing the raw `--release-tag` flag of the deploy and
|
||||
* push commands, parse it into separate arrays of release tag keys and values.
|
||||
@ -114,6 +118,7 @@ export async function loadProject(
|
||||
logger: Logger,
|
||||
opts: ComposeOpts,
|
||||
image?: string,
|
||||
imageTag?: string,
|
||||
): Promise<ComposeProject> {
|
||||
const compose = await import('resin-compose-parse');
|
||||
const { createProject } = await import('./compose');
|
||||
@ -152,7 +157,12 @@ export async function loadProject(
|
||||
}
|
||||
}
|
||||
logger.logDebug('Creating project...');
|
||||
return createProject(opts.projectPath, composeStr, opts.projectName);
|
||||
return createProject(
|
||||
opts.projectPath,
|
||||
composeStr,
|
||||
opts.projectName,
|
||||
imageTag,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -234,7 +244,7 @@ interface Renderer {
|
||||
streams: Dictionary<NodeJS.ReadWriteStream>;
|
||||
}
|
||||
|
||||
export async function buildProject(opts: {
|
||||
export interface BuildProjectOpts {
|
||||
docker: Dockerode;
|
||||
logger: Logger;
|
||||
projectPath: string;
|
||||
@ -247,84 +257,101 @@ export async function buildProject(opts: {
|
||||
inlineLogs?: boolean;
|
||||
convertEol: boolean;
|
||||
dockerfilePath?: string;
|
||||
nogitignore: boolean;
|
||||
nogitignore: boolean; // v13: delete this line
|
||||
multiDockerignore: boolean;
|
||||
}): Promise<BuiltImage[]> {
|
||||
const { logger, projectName } = opts;
|
||||
logger.logInfo(`Building for ${opts.arch}/${opts.deviceType}`);
|
||||
}
|
||||
|
||||
let buildSummaryByService: Dictionary<string> | undefined;
|
||||
export async function buildProject(
|
||||
opts: BuildProjectOpts,
|
||||
): Promise<BuiltImage[]> {
|
||||
await checkBuildSecretsRequirements(opts.docker, opts.projectPath);
|
||||
const compose = await import('resin-compose-parse');
|
||||
const imageDescriptors = compose.parse(opts.composition);
|
||||
const imageDescriptorsByServiceName = _.keyBy(
|
||||
imageDescriptors,
|
||||
'serviceName',
|
||||
);
|
||||
const renderer = await startRenderer({ imageDescriptors, ...opts });
|
||||
let buildSummaryByService: Dictionary<string> | undefined;
|
||||
try {
|
||||
await checkBuildSecretsRequirements(opts.docker, opts.projectPath);
|
||||
|
||||
const needsQemu = await installQemuIfNeeded({ ...opts, imageDescriptors });
|
||||
|
||||
const tarStream = await tarDirectory(opts.projectPath, opts);
|
||||
|
||||
const tasks: BuildTaskPlus[] = await makeBuildTasks(
|
||||
opts.composition,
|
||||
tarStream,
|
||||
const { awaitInterruptibleTask } = await import('./helpers');
|
||||
const [images, summaryMsgByService] = await awaitInterruptibleTask(
|
||||
$buildProject,
|
||||
imageDescriptors,
|
||||
renderer,
|
||||
opts,
|
||||
logger,
|
||||
projectName,
|
||||
);
|
||||
|
||||
setTaskAttributes({ tasks, imageDescriptorsByServiceName, ...opts });
|
||||
|
||||
const transposeOptArray: Array<TransposeOptions | undefined> =
|
||||
await Promise.all(
|
||||
tasks.map((task) => {
|
||||
// Setup emulation if needed
|
||||
if (needsQemu && !task.external) {
|
||||
return qemuTransposeBuildStream({ task, ...opts });
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
await Promise.all(
|
||||
// transposeOptions may be undefined. That's OK.
|
||||
transposeOptArray.map((transposeOptions, index) =>
|
||||
setTaskProgressHooks({
|
||||
task: tasks[index],
|
||||
renderer,
|
||||
transposeOptions,
|
||||
...opts,
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
logger.logDebug('Prepared tasks; building...');
|
||||
|
||||
const { BALENA_ENGINE_TMP_PATH } = await import('../config');
|
||||
const builder = await import('resin-multibuild');
|
||||
|
||||
const builtImages = await builder.performBuilds(
|
||||
tasks,
|
||||
opts.docker,
|
||||
BALENA_ENGINE_TMP_PATH,
|
||||
);
|
||||
|
||||
const [images, summaryMsgByService] = await inspectBuiltImages({
|
||||
builtImages,
|
||||
imageDescriptorsByServiceName,
|
||||
tasks,
|
||||
...opts,
|
||||
});
|
||||
buildSummaryByService = summaryMsgByService;
|
||||
|
||||
return images;
|
||||
} finally {
|
||||
renderer.end(buildSummaryByService);
|
||||
}
|
||||
}
|
||||
|
||||
async function $buildProject(
|
||||
imageDescriptors: ImageDescriptor[],
|
||||
renderer: Renderer,
|
||||
opts: BuildProjectOpts,
|
||||
): Promise<[BuiltImage[], Dictionary<string>]> {
|
||||
const { logger, projectName } = opts;
|
||||
logger.logInfo(`Building for ${opts.arch}/${opts.deviceType}`);
|
||||
|
||||
const needsQemu = await installQemuIfNeeded({ ...opts, imageDescriptors });
|
||||
|
||||
const tarStream = await tarDirectory(opts.projectPath, opts);
|
||||
|
||||
const tasks: BuildTaskPlus[] = await makeBuildTasks(
|
||||
opts.composition,
|
||||
tarStream,
|
||||
opts,
|
||||
logger,
|
||||
projectName,
|
||||
);
|
||||
|
||||
const imageDescriptorsByServiceName = _.keyBy(
|
||||
imageDescriptors,
|
||||
'serviceName',
|
||||
);
|
||||
|
||||
setTaskAttributes({ tasks, imageDescriptorsByServiceName, ...opts });
|
||||
|
||||
const transposeOptArray: Array<TransposeOptions | undefined> =
|
||||
await Promise.all(
|
||||
tasks.map((task) => {
|
||||
// Setup emulation if needed
|
||||
if (needsQemu && !task.external) {
|
||||
return qemuTransposeBuildStream({ task, ...opts });
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
await Promise.all(
|
||||
// transposeOptions may be undefined. That's OK.
|
||||
transposeOptArray.map((transposeOptions, index) =>
|
||||
setTaskProgressHooks({
|
||||
task: tasks[index],
|
||||
renderer,
|
||||
transposeOptions,
|
||||
...opts,
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
logger.logDebug('Prepared tasks; building...');
|
||||
|
||||
const { BALENA_ENGINE_TMP_PATH } = await import('../config');
|
||||
const builder = await import('resin-multibuild');
|
||||
|
||||
const builtImages = await builder.performBuilds(
|
||||
tasks,
|
||||
opts.docker,
|
||||
BALENA_ENGINE_TMP_PATH,
|
||||
);
|
||||
|
||||
return await inspectBuiltImages({
|
||||
builtImages,
|
||||
imageDescriptorsByServiceName,
|
||||
tasks,
|
||||
...opts,
|
||||
});
|
||||
}
|
||||
|
||||
async function startRenderer({
|
||||
imageDescriptors,
|
||||
inlineLogs,
|
||||
@ -390,6 +417,18 @@ async function installQemuIfNeeded({
|
||||
return needsQemu;
|
||||
}
|
||||
|
||||
export function makeImageName(
|
||||
projectName: string,
|
||||
serviceName: string,
|
||||
tag?: string,
|
||||
) {
|
||||
let name = `${projectName}_${serviceName}`;
|
||||
if (tag) {
|
||||
name = [name, tag].map((s) => s.replace(/:/g, '_')).join(':');
|
||||
}
|
||||
return name.toLowerCase();
|
||||
}
|
||||
|
||||
function setTaskAttributes({
|
||||
tasks,
|
||||
buildOpts,
|
||||
@ -405,7 +444,7 @@ function setTaskAttributes({
|
||||
const d = imageDescriptorsByServiceName[task.serviceName];
|
||||
// multibuild (splitBuildStream) parses the composition internally so
|
||||
// any tags we've set before are lost; re-assign them here
|
||||
task.tag ??= [projectName, task.serviceName].join('_').toLowerCase();
|
||||
task.tag ??= makeImageName(projectName, task.serviceName, buildOpts.t);
|
||||
if (isBuildConfig(d.image)) {
|
||||
d.image.tag = task.tag;
|
||||
}
|
||||
@ -686,7 +725,7 @@ export async function getServiceDirsFromComposition(
|
||||
* Return true if `image` is actually a docker-compose.yml `services.service.build`
|
||||
* configuration object, rather than an "external image" (`services.service.image`).
|
||||
*
|
||||
* The `image` argument may therefore refere to either a `build` or `image` property
|
||||
* The `image` argument may therefore refer to either a `build` or `image` property
|
||||
* of a service in a docker-compose.yml file, which is a bit confusing but it matches
|
||||
* the `ImageDescriptor.image` property as defined by `resin-compose-parse`.
|
||||
*
|
||||
@ -717,8 +756,8 @@ export async function tarDirectory(
|
||||
dir: string,
|
||||
param: TarDirectoryOptions,
|
||||
): Promise<import('stream').Readable> {
|
||||
const { nogitignore = false } = param;
|
||||
if (nogitignore) {
|
||||
const { nogitignore = false } = param; // v13: delete this line
|
||||
if (isV13() || nogitignore) {
|
||||
return newTarDirectory(dir, param);
|
||||
} else {
|
||||
return (await import('./compose')).originalTarDirectory(dir, param);
|
||||
@ -739,11 +778,13 @@ async function newTarDirectory(
|
||||
composition,
|
||||
convertEol = false,
|
||||
multiDockerignore = false,
|
||||
nogitignore = false,
|
||||
nogitignore = false, // v13: delete this line
|
||||
preFinalizeCallback,
|
||||
}: TarDirectoryOptions,
|
||||
): Promise<import('stream').Readable> {
|
||||
require('assert').strict.equal(nogitignore, true);
|
||||
if (!isV13()) {
|
||||
require('assert').strict.equal(nogitignore, true);
|
||||
}
|
||||
const { filterFilesWithDockerignore } = await import('./ignore');
|
||||
const { toPosixPath } = (await import('resin-multibuild')).PathUtils;
|
||||
|
||||
@ -859,7 +900,8 @@ function printDockerignoreWarn(
|
||||
}
|
||||
}
|
||||
if (msg.length) {
|
||||
logFunc.call(logger, [' ', hr, ...msg, hr].join('\n'));
|
||||
const { warnify } = require('./messages') as typeof import('./messages');
|
||||
logFunc.call(logger, ' \n' + warnify(msg.join('\n'), ''));
|
||||
}
|
||||
}
|
||||
|
||||
@ -868,11 +910,16 @@ function printDockerignoreWarn(
|
||||
* found and the --gitignore (-g) option has been provided (v11 compatibility).
|
||||
* @param dockerignoreFile Absolute path to a .dockerignore file
|
||||
* @param gitignoreFiles Array of absolute paths to .gitginore files
|
||||
*
|
||||
* v13: delete this function
|
||||
*/
|
||||
export function printGitignoreWarn(
|
||||
dockerignoreFile: string,
|
||||
gitignoreFiles: string[],
|
||||
) {
|
||||
if (isV13()) {
|
||||
return;
|
||||
}
|
||||
const ignoreFiles = [dockerignoreFile, ...gitignoreFiles].filter((e) => e);
|
||||
if (ignoreFiles.length === 0) {
|
||||
return;
|
||||
@ -1004,9 +1051,7 @@ export async function makeBuildTasks(
|
||||
infoStr = `build [${task.context}]`;
|
||||
}
|
||||
logger.logDebug(` ${task.serviceName}: ${infoStr}`);
|
||||
// Workaround for Docker v20.10 + single-arch base images. See:
|
||||
// https://www.flowdock.com/app/rulemotion/i-cli/threads/RuSu1KiWOn62xaGy7O2sn8m8BUc
|
||||
task.dockerPlatform = 'none';
|
||||
task.logger = logger.getAdapter();
|
||||
});
|
||||
|
||||
logger.logDebug(
|
||||
@ -1063,18 +1108,21 @@ async function performResolution(
|
||||
if (!buildTask.buildStream) {
|
||||
continue;
|
||||
}
|
||||
// Consume each task.buildStream in order to trigger the
|
||||
// resolution events that define fields like:
|
||||
// task.dockerfile, task.dockerfilePath,
|
||||
// task.projectType, task.resolved
|
||||
// This mimics what is currently done in `resin-builder`.
|
||||
const clonedStream: Pack = await cloneTarStream(
|
||||
buildTask.buildStream,
|
||||
);
|
||||
buildTask.buildStream = clonedStream;
|
||||
if (!buildTask.external && !buildTask.resolved) {
|
||||
let error: Error | undefined;
|
||||
try {
|
||||
// Consume each task.buildStream in order to trigger the
|
||||
// resolution events that define fields like:
|
||||
// task.dockerfile, task.dockerfilePath,
|
||||
// task.projectType, task.resolved
|
||||
// This mimics what is currently done in `resin-builder`.
|
||||
buildTask.buildStream = await cloneTarStream(buildTask.buildStream);
|
||||
} catch (e) {
|
||||
error = e;
|
||||
}
|
||||
if (error || (!buildTask.external && !buildTask.resolved)) {
|
||||
const cause = error ? `${error}\n` : '';
|
||||
throw new ExpectedError(
|
||||
`Project type for service "${buildTask.serviceName}" could not be determined. Missing a Dockerfile?`,
|
||||
`${cause}Project type for service "${buildTask.serviceName}" could not be determined. Missing a Dockerfile?`,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1285,6 +1333,9 @@ async function pushServiceImages(
|
||||
);
|
||||
}
|
||||
|
||||
// TODO: This should be shared between the CLI & the Builder
|
||||
const PLAIN_SEMVER_REGEX = /^([0-9]+)\.([0-9]+)\.([0-9]+)$/;
|
||||
|
||||
export async function deployProject(
|
||||
docker: import('dockerode'),
|
||||
logger: Logger,
|
||||
@ -1295,6 +1346,8 @@ export async function deployProject(
|
||||
auth: string,
|
||||
apiEndpoint: string,
|
||||
skipLogUpload: boolean,
|
||||
projectPath: string,
|
||||
isDraft: boolean,
|
||||
): Promise<import('balena-release/build/models').ReleaseModel> {
|
||||
const releaseMod = await import('balena-release');
|
||||
const { createRelease, tagServiceImages } = await import('./compose');
|
||||
@ -1303,11 +1356,29 @@ export async function deployProject(
|
||||
const prefix = getChalk().cyan('[Info]') + ' ';
|
||||
const spinner = createSpinner();
|
||||
|
||||
const contractPath = path.join(projectPath, 'balena.yml');
|
||||
const contract = await getContractContent(contractPath);
|
||||
if (contract?.version && !PLAIN_SEMVER_REGEX.test(contract?.version)) {
|
||||
throw new ExpectedError(stripIndent`\
|
||||
Error: expected the version field in "${contractPath}"
|
||||
to be a basic semver in the format '1.2.3'. Got '${contract.version}' instead`);
|
||||
}
|
||||
|
||||
const $release = await runSpinner(
|
||||
tty,
|
||||
spinner,
|
||||
`${prefix}Creating release...`,
|
||||
() => createRelease(apiEndpoint, auth, userId, appId, composition),
|
||||
() =>
|
||||
createRelease(
|
||||
apiEndpoint,
|
||||
auth,
|
||||
userId,
|
||||
appId,
|
||||
composition,
|
||||
isDraft,
|
||||
contract?.version,
|
||||
contract ? JSON.stringify(contract) : undefined,
|
||||
),
|
||||
);
|
||||
const { client: pineClient, release, serviceImages } = $release;
|
||||
|
||||
@ -1315,20 +1386,25 @@ export async function deployProject(
|
||||
logger.logDebug('Tagging images...');
|
||||
const taggedImages = await tagServiceImages(docker, images, serviceImages);
|
||||
try {
|
||||
const token = await getTokenForPreviousRepos(
|
||||
logger,
|
||||
appId,
|
||||
apiEndpoint,
|
||||
taggedImages,
|
||||
);
|
||||
await pushServiceImages(
|
||||
docker,
|
||||
logger,
|
||||
pineClient,
|
||||
taggedImages,
|
||||
token,
|
||||
skipLogUpload,
|
||||
);
|
||||
const { awaitInterruptibleTask } = await import('./helpers');
|
||||
// awaitInterruptibleTask throws SIGINTError on CTRL-C,
|
||||
// causing the release status to be set to 'failed'
|
||||
await awaitInterruptibleTask(async () => {
|
||||
const token = await getTokenForPreviousRepos(
|
||||
logger,
|
||||
appId,
|
||||
apiEndpoint,
|
||||
taggedImages,
|
||||
);
|
||||
await pushServiceImages(
|
||||
docker,
|
||||
logger,
|
||||
pineClient,
|
||||
taggedImages,
|
||||
token,
|
||||
skipLogUpload,
|
||||
);
|
||||
});
|
||||
release.status = 'success';
|
||||
} catch (err) {
|
||||
release.status = 'failed';
|
||||
@ -1392,6 +1468,42 @@ export function createRunLoop(tick: (...args: any[]) => void) {
|
||||
return runloop;
|
||||
}
|
||||
|
||||
async function getContractContent(
|
||||
filePath: string,
|
||||
): Promise<Dictionary<any> | undefined> {
|
||||
let fileContentAsString;
|
||||
try {
|
||||
fileContentAsString = await fs.readFile(filePath, 'utf8');
|
||||
} catch (e) {
|
||||
if (e.code === 'ENOENT') {
|
||||
return; // File does not exist
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
|
||||
let asJson;
|
||||
try {
|
||||
asJson = jsyaml.load(fileContentAsString);
|
||||
} catch (err) {
|
||||
throw new ExpectedError(
|
||||
`Error parsing file "${filePath}":\n ${err.message}`,
|
||||
);
|
||||
}
|
||||
|
||||
if (!isContract(asJson)) {
|
||||
throw new ExpectedError(
|
||||
stripIndent`Error: application contract in '${filePath}' needs to
|
||||
define a top level "type" field with an allowed application type.
|
||||
Allowed application types are: ${allowedContractTypes.join(', ')}`,
|
||||
);
|
||||
}
|
||||
return asJson;
|
||||
}
|
||||
|
||||
function isContract(obj: any): obj is Dictionary<any> {
|
||||
return obj?.type && allowedContractTypes.includes(obj.type);
|
||||
}
|
||||
|
||||
function createLogStream(input: Readable) {
|
||||
const split = require('split') as typeof import('split');
|
||||
const stripAnsi = require('strip-ansi-stream');
|
||||
@ -1532,22 +1644,26 @@ export const composeCliFlags: flags.Input<ComposeCliFlags> = {
|
||||
description:
|
||||
'Hide the image build log output (produce less verbose output)',
|
||||
}),
|
||||
gitignore: flags.boolean({
|
||||
description: stripIndent`
|
||||
Consider .gitignore files in addition to the .dockerignore file. This reverts
|
||||
to the CLI v11 behavior/implementation (deprecated) if compatibility is required
|
||||
until your project can be adapted.`,
|
||||
char: 'g',
|
||||
}),
|
||||
...(isV13()
|
||||
? {}
|
||||
: {
|
||||
gitignore: flags.boolean({
|
||||
description: stripIndent`
|
||||
Consider .gitignore files in addition to the .dockerignore file. This reverts
|
||||
to the CLI v11 behavior/implementation (deprecated) if compatibility is required
|
||||
until your project can be adapted.`,
|
||||
char: 'g',
|
||||
}),
|
||||
nogitignore: flags.boolean({
|
||||
description: `No-op (default behavior) since balena CLI v12.0.0. See "balena help build".`,
|
||||
char: 'G',
|
||||
}),
|
||||
}),
|
||||
'multi-dockerignore': flags.boolean({
|
||||
description:
|
||||
'Have each service use its own .dockerignore file. See "balena help build".',
|
||||
char: 'm',
|
||||
}),
|
||||
nogitignore: flags.boolean({
|
||||
description: `No-op (default behavior) since balena CLI v12.0.0. See "balena help build".`,
|
||||
char: 'G',
|
||||
}),
|
||||
'noparent-check': flags.boolean({
|
||||
description:
|
||||
"Disable project validation check of 'docker-compose.yml' file in parent folder",
|
||||
@ -1566,8 +1682,9 @@ export const composeCliFlags: flags.Input<ComposeCliFlags> = {
|
||||
"Don't convert line endings from CRLF (Windows format) to LF (Unix format).",
|
||||
}),
|
||||
projectName: flags.string({
|
||||
description:
|
||||
'Specify an alternate project name; default is the directory name',
|
||||
description: stripIndent`\
|
||||
Name prefix for locally built images. This is the 'projectName' portion
|
||||
in 'projectName_serviceName:tag'. The default is the directory name.`,
|
||||
char: 'n',
|
||||
}),
|
||||
};
|
||||
|
@ -20,6 +20,7 @@ import * as os from 'os';
|
||||
import * as request from 'request';
|
||||
import type * as Stream from 'stream';
|
||||
|
||||
import { retry } from '../helpers';
|
||||
import Logger = require('../logger');
|
||||
import * as ApiErrors from './errors';
|
||||
|
||||
@ -211,17 +212,21 @@ export class DeviceAPI {
|
||||
);
|
||||
return;
|
||||
}
|
||||
res.socket.setKeepAlive(true, 1000);
|
||||
if (os.platform() !== 'win32') {
|
||||
const NetKeepalive = await import('net-keepalive');
|
||||
// Certain versions of typescript won't convert
|
||||
// this automatically
|
||||
const sock = res.socket as any as NodeJSSocketWithFileDescriptor;
|
||||
// We send a tcp keepalive probe once every 5 seconds
|
||||
NetKeepalive.setKeepAliveInterval(sock, 5000);
|
||||
// After 5 failed probes, the connection is marked as
|
||||
// closed
|
||||
NetKeepalive.setKeepAliveProbes(sock, 5);
|
||||
try {
|
||||
res.socket.setKeepAlive(true, 1000);
|
||||
if (os.platform() !== 'win32') {
|
||||
const NetKeepalive = await import('net-keepalive');
|
||||
// Certain versions of typescript won't convert
|
||||
// this automatically
|
||||
const sock = res.socket as any as NodeJSSocketWithFileDescriptor;
|
||||
// We send a tcp keepalive probe once every 5 seconds
|
||||
NetKeepalive.setKeepAliveInterval(sock, 5000);
|
||||
// After 5 failed probes, the connection is marked as
|
||||
// closed
|
||||
NetKeepalive.setKeepAliveProbes(sock, 5);
|
||||
}
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
resolve(res);
|
||||
});
|
||||
@ -256,24 +261,35 @@ export class DeviceAPI {
|
||||
}
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
return request(opts, (err, response, body) => {
|
||||
if (err) {
|
||||
return reject(err);
|
||||
}
|
||||
switch (response.statusCode) {
|
||||
case 200:
|
||||
return resolve(body);
|
||||
case 400:
|
||||
return reject(new ApiErrors.BadRequestDeviceAPIError(body.message));
|
||||
case 503:
|
||||
return reject(
|
||||
new ApiErrors.ServiceUnavailableAPIError(body.message),
|
||||
);
|
||||
default:
|
||||
return reject(new ApiErrors.DeviceAPIError(body.message));
|
||||
}
|
||||
const doRequest = async () => {
|
||||
return await new Promise((resolve, reject) => {
|
||||
return request(opts, (err, response, body) => {
|
||||
if (err) {
|
||||
return reject(err);
|
||||
}
|
||||
switch (response.statusCode) {
|
||||
case 200:
|
||||
return resolve(body);
|
||||
case 400:
|
||||
return reject(
|
||||
new ApiErrors.BadRequestDeviceAPIError(body.message),
|
||||
);
|
||||
case 503:
|
||||
return reject(
|
||||
new ApiErrors.ServiceUnavailableAPIError(body.message),
|
||||
);
|
||||
default:
|
||||
return reject(new ApiErrors.DeviceAPIError(body.message));
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
return await retry({
|
||||
func: doRequest,
|
||||
initialDelayMs: 2000,
|
||||
maxAttempts: 6,
|
||||
label: `Supervisor API (${opts.method} ${(opts as any).url})`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -57,7 +57,7 @@ export interface DeviceDeployOptions {
|
||||
registrySecrets: RegistrySecrets;
|
||||
multiDockerignore: boolean;
|
||||
nocache: boolean;
|
||||
nogitignore: boolean;
|
||||
nogitignore: boolean; // v13: delete this line
|
||||
noParentCheck: boolean;
|
||||
nolive: boolean;
|
||||
pull: boolean;
|
||||
@ -182,7 +182,7 @@ export async function deployToDevice(opts: DeviceDeployOptions): Promise<void> {
|
||||
convertEol: opts.convertEol,
|
||||
dockerfilePath: opts.dockerfilePath,
|
||||
multiDockerignore: opts.multiDockerignore,
|
||||
nogitignore: opts.nogitignore,
|
||||
nogitignore: opts.nogitignore, // v13: delete this line
|
||||
noParentCheck: opts.noParentCheck,
|
||||
projectName: 'local',
|
||||
projectPath: opts.source,
|
||||
@ -201,7 +201,7 @@ export async function deployToDevice(opts: DeviceDeployOptions): Promise<void> {
|
||||
composition: project.composition,
|
||||
convertEol: opts.convertEol,
|
||||
multiDockerignore: opts.multiDockerignore,
|
||||
nogitignore: opts.nogitignore,
|
||||
nogitignore: opts.nogitignore, // v13: delete this line
|
||||
});
|
||||
|
||||
// Try to detect the device information
|
||||
@ -310,7 +310,7 @@ function connectToDocker(host: string, port: number): Docker {
|
||||
});
|
||||
}
|
||||
|
||||
export async function performBuilds(
|
||||
async function performBuilds(
|
||||
composition: Composition,
|
||||
tarStream: Readable,
|
||||
docker: Docker,
|
||||
@ -426,7 +426,7 @@ export async function rebuildSingleTask(
|
||||
composition,
|
||||
convertEol: opts.convertEol,
|
||||
multiDockerignore: opts.multiDockerignore,
|
||||
nogitignore: opts.nogitignore,
|
||||
nogitignore: opts.nogitignore, // v13: delete this line
|
||||
});
|
||||
|
||||
const task = _.find(
|
||||
|
@ -80,16 +80,19 @@ async function displayDeviceLogs(
|
||||
jsonStream.on('error', (e) => {
|
||||
logger.logWarn(`Error parsing NDJSON log chunk: ${e}`);
|
||||
});
|
||||
logs.once('error', reject);
|
||||
logs.once('end', () => {
|
||||
logs.once('error', handleError);
|
||||
logs.once('end', handleError);
|
||||
logs.pipe(jsonStream);
|
||||
|
||||
function handleError(error?: Error | string) {
|
||||
logger.logWarn(DeviceConnectionLostError.defaultMsg);
|
||||
if (gotSignal) {
|
||||
reject(new SIGINTError('Log streaming aborted on SIGINT signal'));
|
||||
} else {
|
||||
reject(new DeviceConnectionLostError());
|
||||
const msg = typeof error === 'string' ? error : error?.message;
|
||||
reject(new DeviceConnectionLostError(msg));
|
||||
}
|
||||
});
|
||||
logs.pipe(jsonStream);
|
||||
}
|
||||
});
|
||||
} finally {
|
||||
process.removeListener('SIGINT', handleSignal);
|
||||
|
@ -72,7 +72,9 @@ export const dockerConnectionCliFlags: flags.Input<DockerConnectionCliFlags> = {
|
||||
|
||||
export const dockerCliFlags: flags.Input<DockerCliFlags> = {
|
||||
tag: flags.string({
|
||||
description: 'The alias to the generated image',
|
||||
description: `\
|
||||
Tag locally built Docker images. This is the 'tag' portion
|
||||
in 'projectName_serviceName:tag'. The default is 'latest'.`,
|
||||
char: 't',
|
||||
}),
|
||||
buildArg: flags.string({
|
||||
@ -105,7 +107,7 @@ export interface BuildOpts {
|
||||
pull?: boolean;
|
||||
registryconfig?: import('resin-multibuild').RegistrySecrets;
|
||||
squash?: boolean;
|
||||
t?: string;
|
||||
t?: string; // only the tag portion of the image name, e.g. 'abc' in 'myimg:abc'
|
||||
}
|
||||
|
||||
function parseBuildArgs(args: string[]): Dictionary<string> {
|
||||
|
@ -148,23 +148,18 @@ export async function osProgressHandler(step: InitializeEmitter) {
|
||||
});
|
||||
}
|
||||
|
||||
export function getAppWithArch(
|
||||
export async function getAppWithArch(
|
||||
applicationName: string,
|
||||
): Promise<ApplicationWithDeviceType & { arch: string }> {
|
||||
return Promise.all([
|
||||
getApplication(applicationName),
|
||||
getBalenaSdk().models.config.getDeviceTypes(),
|
||||
]).then(function ([app, deviceTypes]) {
|
||||
const config = _.find<BalenaSdk.DeviceTypeJson.DeviceType>(deviceTypes, {
|
||||
slug: app.is_for__device_type[0].slug,
|
||||
});
|
||||
const app = await getApplication(applicationName);
|
||||
const { getExpanded } = await import('./pine');
|
||||
|
||||
if (!config) {
|
||||
throw new Error(`balena API request failed for fleet ${applicationName}`);
|
||||
}
|
||||
|
||||
return { ...app, arch: config.arch };
|
||||
});
|
||||
return {
|
||||
...app,
|
||||
arch: getExpanded(
|
||||
getExpanded(app.is_for__device_type)!.is_of__cpu_architecture,
|
||||
)!.slug,
|
||||
};
|
||||
}
|
||||
|
||||
// TODO: Drop this. The sdk now has this baked in application.get().
|
||||
@ -182,6 +177,11 @@ function getApplication(
|
||||
},
|
||||
is_for__device_type: {
|
||||
$select: 'slug',
|
||||
$expand: {
|
||||
is_of__cpu_architecture: {
|
||||
$select: 'slug',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
@ -463,13 +463,27 @@ export function getProxyConfig(): ProxyConfig | undefined {
|
||||
}
|
||||
}
|
||||
|
||||
export const expandForAppName: BalenaSdk.PineOptions<BalenaSdk.Device> = {
|
||||
export const expandForAppName = {
|
||||
$expand: {
|
||||
belongs_to__application: { $select: 'app_name' },
|
||||
is_of__device_type: { $select: 'slug' },
|
||||
is_running__release: { $select: 'commit' },
|
||||
},
|
||||
};
|
||||
} as const;
|
||||
|
||||
export const expandForAppNameAndCpuArch = {
|
||||
$expand: {
|
||||
...expandForAppName.$expand,
|
||||
is_of__device_type: {
|
||||
$select: 'slug',
|
||||
$expand: {
|
||||
is_of__cpu_architecture: {
|
||||
$select: 'slug',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* Use the `readline` library on Windows to install SIGINT handlers.
|
||||
|
@ -27,6 +27,7 @@ import { ExpectedError } from '../errors';
|
||||
|
||||
const { toPosixPath } = MultiBuild.PathUtils;
|
||||
|
||||
// v13: delete this enum
|
||||
export enum IgnoreFileType {
|
||||
DockerIgnore,
|
||||
GitIgnore,
|
||||
@ -42,6 +43,8 @@ interface IgnoreEntry {
|
||||
* This class is used by the CLI v10 / v11 "original" tarDirectory function
|
||||
* in `compose.js`. It is still around for the benefit of the `--gitignore`
|
||||
* option, but is expected to be deleted in CLI v13.
|
||||
*
|
||||
* v13: delete this class
|
||||
*/
|
||||
export class FileIgnorer {
|
||||
private dockerIgnoreEntries: IgnoreEntry[];
|
||||
|
@ -30,6 +30,14 @@ enum Level {
|
||||
LIVEPUSH = 'livepush',
|
||||
}
|
||||
|
||||
interface LoggerAdapter {
|
||||
debug: (msg: string) => void;
|
||||
error: (msg: string) => void;
|
||||
info: (msg: string) => void;
|
||||
log: (msg: string) => void;
|
||||
warn: (msg: string) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* General purpose logger class with support for log streams and colours.
|
||||
* Call `Logger.getLogger()` to retrieve a global shared instance of this
|
||||
@ -57,6 +65,8 @@ class Logger {
|
||||
|
||||
protected deferredLogMessages: Array<[string, Level]>;
|
||||
|
||||
protected adapter: LoggerAdapter;
|
||||
|
||||
protected constructor() {
|
||||
const logger = new StreamLogger();
|
||||
const chalk = getChalk();
|
||||
@ -91,6 +101,14 @@ class Logger {
|
||||
this.formatMessage = logger.formatWithPrefix.bind(logger);
|
||||
|
||||
this.deferredLogMessages = [];
|
||||
|
||||
this.adapter = {
|
||||
debug: (msg: string) => this.logDebug(msg),
|
||||
error: (msg: string) => this.logError(msg),
|
||||
info: (msg: string) => this.logInfo(msg),
|
||||
log: (msg: string) => this.logLogs(msg),
|
||||
warn: (msg: string) => this.logWarn(msg),
|
||||
};
|
||||
}
|
||||
|
||||
protected static logger: Logger;
|
||||
@ -151,6 +169,10 @@ class Logger {
|
||||
});
|
||||
this.deferredLogMessages = [];
|
||||
}
|
||||
|
||||
public getAdapter(): LoggerAdapter {
|
||||
return this.adapter;
|
||||
}
|
||||
}
|
||||
|
||||
export = Logger;
|
||||
|
@ -15,6 +15,8 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { isV13 } from './version';
|
||||
|
||||
export const reachingOut = `\
|
||||
For further help or support, visit:
|
||||
https://www.balena.io/docs/reference/balena-cli/#support-faq-and-troubleshooting
|
||||
@ -30,6 +32,12 @@ export const help = reachingOut;
|
||||
// is parsed, so its evaluation cannot happen at module loading time.
|
||||
export const getHelp = () => (process.env.DEBUG ? '' : debugHint) + help;
|
||||
|
||||
export const deprecationPolicyNote = `\
|
||||
The balena CLI enforces its deprecation policy by exiting with an error a year
|
||||
after the release of the next major version, unless the --unsupported option is
|
||||
used. Find out more at: https://git.io/JRHUW#deprecation-policy
|
||||
`;
|
||||
|
||||
/**
|
||||
* Take a multiline string like:
|
||||
* Line One
|
||||
@ -41,8 +49,9 @@ export const getHelp = () => (process.env.DEBUG ? '' : debugHint) + help;
|
||||
* ---------------
|
||||
* where the length of the dash rows matches the length of the longest line.
|
||||
*/
|
||||
export function warnify(msg: string) {
|
||||
const lines = msg.split('\n').map((l) => `[Warn] ${l}`);
|
||||
export function warnify(msg: string, prefix = '[Warn] ') {
|
||||
let lines = msg.split('\n');
|
||||
lines = prefix ? lines.map((l) => `${prefix}${l}`) : lines;
|
||||
const maxLength = Math.max(...lines.map((l) => l.length));
|
||||
const hr = '-'.repeat(maxLength);
|
||||
return [hr, ...lines, hr].join('\n');
|
||||
@ -79,7 +88,7 @@ If the --registry-secrets option is not specified, and a secrets.yml or
|
||||
secrets.json file exists in the balena directory (usually $HOME/.balena),
|
||||
this file will be used instead.`;
|
||||
|
||||
export const dockerignoreHelp =
|
||||
const dockerignoreHelpV12 =
|
||||
'DOCKERIGNORE AND GITIGNORE FILES \n' +
|
||||
`By default, the balena CLI will use a single ".dockerignore" file (if any) at
|
||||
the project root (--source directory) in order to decide which source files to
|
||||
@ -132,6 +141,60 @@ adding counter patterns to the applicable .dockerignore file(s), for example
|
||||
- https://docs.docker.com/engine/reference/builder/#dockerignore-file
|
||||
- https://www.npmjs.com/package/@balena/dockerignore`;
|
||||
|
||||
const dockerignoreHelpV13 =
|
||||
'DOCKERIGNORE AND GITIGNORE FILES \n' +
|
||||
`By default, the balena CLI will use a single ".dockerignore" file (if any) at
|
||||
the project root (--source directory) in order to decide which source files to
|
||||
exclude from the "build context" (tar stream) sent to balenaCloud, Docker
|
||||
daemon or balenaEngine. In a microservices (multicontainer) fleet, the
|
||||
source directory is the directory that contains the "docker-compose.yml" file.
|
||||
|
||||
The --multi-dockerignore (-m) option may be used with microservices
|
||||
(multicontainer) fleets that define a docker-compose.yml file. When this
|
||||
option is used, each service subdirectory (defined by the \`build\` or
|
||||
\`build.context\` service properties in the docker-compose.yml file) is
|
||||
filtered separately according to a .dockerignore file defined in the service
|
||||
subdirectory. If no .dockerignore file exists in a service subdirectory, then
|
||||
only the default .dockerignore patterns (see below) apply for that service
|
||||
subdirectory.
|
||||
|
||||
When the --multi-dockerignore (-m) option is used, the .dockerignore file (if
|
||||
any) defined at the overall project root will be used to filter files and
|
||||
subdirectories other than service subdirectories. It will not have any effect
|
||||
on service subdirectories, whether or not a service subdirectory defines its
|
||||
own .dockerignore file. Multiple .dockerignore files are not merged or added
|
||||
together, and cannot override or extend other files. This behavior maximizes
|
||||
compatibility with the standard docker-compose tool, while still allowing a
|
||||
root .dockerignore file (at the overall project root) to filter files and
|
||||
folders that are outside service subdirectories.
|
||||
|
||||
balena CLI v11 also took .gitignore files into account. This behavior was
|
||||
deprecated in CLI v12 and removed in CLI v13. Please use .dockerignore files
|
||||
instead.
|
||||
|
||||
Default .dockerignore patterns \n` +
|
||||
`A few default/hardcoded dockerignore patterns are "merged" (in memory) with the
|
||||
patterns found in the applicable .dockerignore files, in the following order:
|
||||
\`\`\`
|
||||
**/.git
|
||||
< user's patterns from the applicable '.dockerignore' file, if any >
|
||||
!**/.balena
|
||||
!**/.resin
|
||||
!**/Dockerfile
|
||||
!**/Dockerfile.*
|
||||
!**/docker-compose.yml
|
||||
\`\`\`
|
||||
These patterns always apply, whether or not .dockerignore files exist in the
|
||||
project. If necessary, the effect of the \`**/.git\` pattern may be modified by
|
||||
adding exception patterns to the applicable .dockerignore file(s), for example
|
||||
\`!mysubmodule/.git\`. For documentation on pattern format, see:
|
||||
- https://docs.docker.com/engine/reference/builder/#dockerignore-file
|
||||
- https://www.npmjs.com/package/@balena/dockerignore`;
|
||||
|
||||
export const dockerignoreHelp = isV13()
|
||||
? dockerignoreHelpV13
|
||||
: dockerignoreHelpV12;
|
||||
|
||||
export const applicationIdInfo = `\
|
||||
Fleets may be specified by fleet name, slug, or numeric ID. Fleet slugs are
|
||||
the recommended option, as they are unique and unambiguous. Slugs can be
|
||||
@ -145,6 +208,16 @@ environments). Numeric fleet IDs are deprecated because they consist of an
|
||||
implementation detail of the balena backend. We intend to remove support for
|
||||
numeric IDs at some point in the future.`;
|
||||
|
||||
export const applicationNameNote = `\
|
||||
Fleets may be specified by fleet name or slug. Slugs are recommended because
|
||||
they are unique and unambiguous. Slugs can be listed with the \`balena fleets\`
|
||||
command. Note that slugs may change if the fleet is renamed. Fleet names are
|
||||
not unique and may result in "Fleet is ambiguous" errors at any time (even if
|
||||
"it used to work in the past"), for example if the name clashes with a newly
|
||||
created public/open fleet, or with fleets from other balena accounts that you
|
||||
may be invited to join under any role. For this reason, fleet names are
|
||||
especially discouraged in scripts (e.g. CI environments).`;
|
||||
|
||||
export const jsonInfo = `\
|
||||
The --json option is recommended when scripting the output of this command,
|
||||
because field names are less likely to change in JSON format and because it
|
||||
@ -184,3 +257,13 @@ the next major version of the CLI (v13). The --v13 option may be used
|
||||
to enable the new names already now, and suppress a warning message.
|
||||
(The --v13 option will be silently ignored in CLI v13.)
|
||||
Find out more at: https://git.io/JRuZr`;
|
||||
|
||||
export function getNodeEngineVersionWarn(
|
||||
version: string,
|
||||
validVersions: string,
|
||||
) {
|
||||
version = version.startsWith('v') ? version.substring(1) : version;
|
||||
return warnify(`\
|
||||
Node.js version "${version}" does not satisfy requirement "${validVersions}"
|
||||
This may cause unexpected behavior.`);
|
||||
}
|
||||
|
@ -16,18 +16,12 @@ limitations under the License.
|
||||
import type * as BalenaSdk from 'balena-sdk';
|
||||
import _ = require('lodash');
|
||||
|
||||
import {
|
||||
exitWithExpectedError,
|
||||
instanceOf,
|
||||
NotLoggedInError,
|
||||
ExpectedError,
|
||||
} from '../errors';
|
||||
import { instanceOf, NotLoggedInError, ExpectedError } from '../errors';
|
||||
import { getBalenaSdk, getVisuals, stripIndent, getCliForm } from './lazy';
|
||||
import validation = require('./validation');
|
||||
import { delay } from './helpers';
|
||||
import { isV13 } from './version';
|
||||
import type { Application, Device, Organization } from 'balena-sdk';
|
||||
import { getApplication } from './sdk';
|
||||
|
||||
export function authenticate(options: {}): Promise<void> {
|
||||
const balena = getBalenaSdk();
|
||||
@ -135,18 +129,16 @@ export function selectDeviceType() {
|
||||
|
||||
/**
|
||||
* Display interactive confirmation prompt.
|
||||
* If the user declines, then either an error will be thrown,
|
||||
* or `exitWithExpectedError` will be called (if exitIfDeclined true).
|
||||
* Throw ExpectedError if the user declines.
|
||||
* @param yesOption - automatically confirm if true
|
||||
* @param message - message to display with prompt
|
||||
* @param yesMessage - message to display if automatically confirming
|
||||
* @param exitIfDeclined - exitWithExpectedError when decline if true
|
||||
*/
|
||||
export async function confirm(
|
||||
yesOption: boolean,
|
||||
message: string,
|
||||
yesMessage?: string,
|
||||
exitIfDeclined = false,
|
||||
defaultValue = false,
|
||||
) {
|
||||
if (yesOption) {
|
||||
if (yesMessage) {
|
||||
@ -162,16 +154,11 @@ export async function confirm(
|
||||
const confirmed = await getCliForm().ask<boolean>({
|
||||
message,
|
||||
type: 'confirm',
|
||||
default: false,
|
||||
default: defaultValue,
|
||||
});
|
||||
|
||||
if (!confirmed) {
|
||||
const err = new ExpectedError('Aborted');
|
||||
// TODO remove this deprecated function (exitWithExpectedError)
|
||||
if (exitIfDeclined) {
|
||||
exitWithExpectedError(err);
|
||||
}
|
||||
throw err;
|
||||
throw new ExpectedError('Aborted');
|
||||
}
|
||||
}
|
||||
|
||||
@ -281,11 +268,9 @@ export async function awaitDeviceOsUpdate(
|
||||
}
|
||||
|
||||
if (osUpdateStatus.error) {
|
||||
console.error(
|
||||
`Failed to complete Host OS update on device ${deviceName}!`,
|
||||
throw new ExpectedError(
|
||||
`Failed to complete Host OS update on device ${deviceName}\n${osUpdateStatus.error}`,
|
||||
);
|
||||
exitWithExpectedError(osUpdateStatus.error);
|
||||
return;
|
||||
}
|
||||
|
||||
if (osUpdateProgress !== null) {
|
||||
@ -379,6 +364,7 @@ export async function getOnlineTargetDeviceUuid(
|
||||
let app: Application;
|
||||
try {
|
||||
logger.logDebug(`Fetching fleet ${applicationOrDevice}`);
|
||||
const { getApplication } = await import('./sdk');
|
||||
app = await getApplication(sdk, applicationOrDevice);
|
||||
} catch (err) {
|
||||
const { BalenaApplicationNotFound } = await import('balena-errors');
|
||||
|
@ -19,6 +19,7 @@ import type * as BalenaSdk from 'balena-sdk';
|
||||
import { ExpectedError, printErrorMessage } from '../errors';
|
||||
import { getVisuals, stripIndent, getCliForm } from './lazy';
|
||||
import Logger = require('./logger');
|
||||
import { confirm } from './patterns';
|
||||
import { exec, execBuffered, getDeviceOsRelease } from './ssh';
|
||||
|
||||
const MIN_BALENAOS_VERSION = 'v2.14.0';
|
||||
@ -211,7 +212,7 @@ async function getOrSelectApplication(
|
||||
.value();
|
||||
|
||||
if (!appName) {
|
||||
return createOrSelectAppOrExit(sdk, compatibleDeviceTypes, deviceType);
|
||||
return createOrSelectApp(sdk, compatibleDeviceTypes, deviceType);
|
||||
}
|
||||
|
||||
const options: BalenaSdk.PineOptions<BalenaSdk.Application> = {
|
||||
@ -239,17 +240,14 @@ async function getOrSelectApplication(
|
||||
)) as ApplicationWithDeviceType[];
|
||||
|
||||
if (applications.length === 0) {
|
||||
const shouldCreateApp = await getCliForm().ask({
|
||||
message:
|
||||
`No fleet found with name "${appName}".\n` +
|
||||
await confirm(
|
||||
false,
|
||||
`No fleet found with name "${appName}".\n` +
|
||||
'Would you like to create it now?',
|
||||
type: 'confirm',
|
||||
default: true,
|
||||
});
|
||||
if (shouldCreateApp) {
|
||||
return createApplication(sdk, deviceType, name);
|
||||
}
|
||||
process.exit(1);
|
||||
undefined,
|
||||
true,
|
||||
);
|
||||
return await createApplication(sdk, deviceType, name);
|
||||
}
|
||||
|
||||
// We've found at least one fleet with the given name.
|
||||
@ -269,10 +267,7 @@ async function getOrSelectApplication(
|
||||
return selectAppFromList(applications);
|
||||
}
|
||||
|
||||
// TODO: revisit this function's purpose. It was refactored out of
|
||||
// `getOrSelectApplication` above in order to satisfy some resin-lint v3
|
||||
// rules, but it looks like there's a fair amount of duplicate logic.
|
||||
async function createOrSelectAppOrExit(
|
||||
async function createOrSelectApp(
|
||||
sdk: BalenaSdk.BalenaSDK,
|
||||
compatibleDeviceTypes: string[],
|
||||
deviceType: string,
|
||||
@ -291,17 +286,14 @@ async function createOrSelectAppOrExit(
|
||||
})) as ApplicationWithDeviceType[];
|
||||
|
||||
if (applications.length === 0) {
|
||||
const shouldCreateApp = await getCliForm().ask({
|
||||
message:
|
||||
'You have no fleets this device can join.\n' +
|
||||
await confirm(
|
||||
false,
|
||||
'You have no fleets this device can join.\n' +
|
||||
'Would you like to create one now?',
|
||||
type: 'confirm',
|
||||
default: true,
|
||||
});
|
||||
if (shouldCreateApp) {
|
||||
return createApplication(sdk, deviceType);
|
||||
}
|
||||
process.exit(1);
|
||||
undefined,
|
||||
true,
|
||||
);
|
||||
return await createApplication(sdk, deviceType);
|
||||
}
|
||||
|
||||
return selectAppFromList(applications);
|
||||
|
@ -21,7 +21,7 @@ import { ExpectedError } from '../errors';
|
||||
import { getBalenaSdk, stripIndent } from './lazy';
|
||||
import Logger = require('./logger');
|
||||
|
||||
export const QEMU_VERSION = 'v5.2.0+balena4';
|
||||
export const QEMU_VERSION = 'v6.0.0+balena1';
|
||||
export const QEMU_BIN_NAME = 'qemu-execve';
|
||||
|
||||
export function qemuPathInContext(context: string) {
|
||||
|
@ -42,6 +42,7 @@ export interface BuildOpts {
|
||||
headless: boolean;
|
||||
convertEol: boolean;
|
||||
multiDockerignore: boolean;
|
||||
isDraft: boolean;
|
||||
}
|
||||
|
||||
export interface RemoteBuild {
|
||||
@ -49,7 +50,7 @@ export interface RemoteBuild {
|
||||
source: string;
|
||||
auth: string;
|
||||
baseUrl: string;
|
||||
nogitignore: boolean;
|
||||
nogitignore: boolean; // v13: delete this line
|
||||
opts: BuildOpts;
|
||||
sdk: BalenaSDK;
|
||||
// For internal use
|
||||
@ -92,6 +93,7 @@ async function getBuilderEndpoint(
|
||||
emulated: opts.emulated,
|
||||
nocache: opts.nocache,
|
||||
headless: opts.headless,
|
||||
isdraft: opts.isDraft,
|
||||
});
|
||||
// Note that using https (rather than http) is a requirement when using the
|
||||
// --registry-secrets feature, as the secrets are not otherwise encrypted.
|
||||
@ -319,7 +321,7 @@ async function getTarStream(build: RemoteBuild): Promise<Stream.Readable> {
|
||||
preFinalizeCallback: preFinalizeCb,
|
||||
convertEol: build.opts.convertEol,
|
||||
multiDockerignore: build.opts.multiDockerignore,
|
||||
nogitignore: build.nogitignore,
|
||||
nogitignore: build.nogitignore, // v13: delete this line
|
||||
});
|
||||
} finally {
|
||||
tarSpinner.stop();
|
||||
|
606
npm-shrinkwrap.json
generated
606
npm-shrinkwrap.json
generated
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "balena-cli",
|
||||
"version": "12.45.0",
|
||||
"version": "12.50.2",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
@ -1300,12 +1300,12 @@
|
||||
"integrity": "sha512-u86QDMtkpHLlvehs3Z+yHklXRhDPL5XGCO3BCSuaD61gKzrNDUIj03cz8T/PBPPUJqn7DfWkf9sKP9VwlvxKuw=="
|
||||
},
|
||||
"@balena/node-web-streams": {
|
||||
"version": "0.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@balena/node-web-streams/-/node-web-streams-0.2.3.tgz",
|
||||
"integrity": "sha512-WaFtrO5lQUAWmLVcBn7V0tLHOuX/S9JPxmhfcEc9drLZhNUKF/psnNwWGfhPxgRwwik4hK0AqKIbjqkLLBXhSg==",
|
||||
"version": "0.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@balena/node-web-streams/-/node-web-streams-0.2.4.tgz",
|
||||
"integrity": "sha512-Q9By3GPzANMZuf1i5i7Agyh6BUe6tTa+VCCZzsFzU32iXMcuDRXYHbNIKESrcjVXxiZScPB4u++WPw4LRyK1Gg==",
|
||||
"requires": {
|
||||
"is-stream": "^1.1.0",
|
||||
"web-streams-polyfill": "^1.3.2"
|
||||
"web-streams-polyfill": "^3.1.0"
|
||||
}
|
||||
},
|
||||
"@balena/udif": {
|
||||
@ -2169,68 +2169,68 @@
|
||||
"integrity": "sha512-STcqSvk+c7ArMrZgYxhM92p6O6F7t0SUbGr+zm8s9fJple5EdJAMwP3dXqgdXeF95xWhBpha5kjEqNAIdI0r4w=="
|
||||
},
|
||||
"@sentry/core": {
|
||||
"version": "5.25.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/core/-/core-5.25.0.tgz",
|
||||
"integrity": "sha512-hY6Zmo7t/RV+oZuvXHP6nyAj/QnZr2jW0e7EbL5YKMV8q0vlnjcE0LgqFXme726OJemoLk67z+sQOJic/Ztehg==",
|
||||
"version": "6.13.2",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/core/-/core-6.13.2.tgz",
|
||||
"integrity": "sha512-snXNNFLwlS7yYxKTX4DBXebvJK+6ikBWN6noQ1CHowvM3ReFBlrdrs0Z0SsSFEzXm2S4q7f6HHbm66GSQZ/8FQ==",
|
||||
"requires": {
|
||||
"@sentry/hub": "5.25.0",
|
||||
"@sentry/minimal": "5.25.0",
|
||||
"@sentry/types": "5.25.0",
|
||||
"@sentry/utils": "5.25.0",
|
||||
"@sentry/hub": "6.13.2",
|
||||
"@sentry/minimal": "6.13.2",
|
||||
"@sentry/types": "6.13.2",
|
||||
"@sentry/utils": "6.13.2",
|
||||
"tslib": "^1.9.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"tslib": {
|
||||
"version": "1.14.0",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.0.tgz",
|
||||
"integrity": "sha512-+Zw5lu0D9tvBMjGP8LpvMb0u2WW2QV3y+D8mO6J+cNzCYIN4sVy43Bf9vl92nqFahutN0I8zHa7cc4vihIshnw=="
|
||||
"version": "1.14.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
|
||||
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"@sentry/hub": {
|
||||
"version": "5.25.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/hub/-/hub-5.25.0.tgz",
|
||||
"integrity": "sha512-kOlOiJV8wMX50lYpzMlOXBoH7MNG0Ho4RTusdZnXZBaASq5/ljngDJkLr6uylNjceZQP21wzipCQajsJMYB7EQ==",
|
||||
"version": "6.13.2",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/hub/-/hub-6.13.2.tgz",
|
||||
"integrity": "sha512-sppSuJdNMiMC/vFm/dQowCBh11uTrmvks00fc190YWgxHshodJwXMdpc+pN61VSOmy2QA4MbQ5aMAgHzPzel3A==",
|
||||
"requires": {
|
||||
"@sentry/types": "5.25.0",
|
||||
"@sentry/utils": "5.25.0",
|
||||
"@sentry/types": "6.13.2",
|
||||
"@sentry/utils": "6.13.2",
|
||||
"tslib": "^1.9.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"tslib": {
|
||||
"version": "1.14.0",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.0.tgz",
|
||||
"integrity": "sha512-+Zw5lu0D9tvBMjGP8LpvMb0u2WW2QV3y+D8mO6J+cNzCYIN4sVy43Bf9vl92nqFahutN0I8zHa7cc4vihIshnw=="
|
||||
"version": "1.14.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
|
||||
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"@sentry/minimal": {
|
||||
"version": "5.25.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/minimal/-/minimal-5.25.0.tgz",
|
||||
"integrity": "sha512-9JFKuW7U+1vPO86k3+XRtJyooiVZsVOsFFO4GulBzepi3a0ckNyPgyjUY1saLH+cEHx18hu8fGgajvI8ANUF2g==",
|
||||
"version": "6.13.2",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/minimal/-/minimal-6.13.2.tgz",
|
||||
"integrity": "sha512-6iJfEvHzzpGBHDfLxSHcGObh73XU1OSQKWjuhDOe7UQDyI4BQmTfcXAC+Fr8sm8C/tIsmpVi/XJhs8cubFdSMw==",
|
||||
"requires": {
|
||||
"@sentry/hub": "5.25.0",
|
||||
"@sentry/types": "5.25.0",
|
||||
"@sentry/hub": "6.13.2",
|
||||
"@sentry/types": "6.13.2",
|
||||
"tslib": "^1.9.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"tslib": {
|
||||
"version": "1.14.0",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.0.tgz",
|
||||
"integrity": "sha512-+Zw5lu0D9tvBMjGP8LpvMb0u2WW2QV3y+D8mO6J+cNzCYIN4sVy43Bf9vl92nqFahutN0I8zHa7cc4vihIshnw=="
|
||||
"version": "1.14.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
|
||||
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"@sentry/node": {
|
||||
"version": "5.25.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/node/-/node-5.25.0.tgz",
|
||||
"integrity": "sha512-zxoUVdAFTeK9kdEGY95TMs6g8Zx/P55HxG4gHD80BG/XIEvWiGPcGCLOspO4IdGqYXkGS74KfBOIXmmCawWwLg==",
|
||||
"version": "6.13.2",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/node/-/node-6.13.2.tgz",
|
||||
"integrity": "sha512-0Vw22amG143MTiNaSny66YGU3+uW7HxyGI9TLGE7aJY1nNmC0DE+OgqQYGBRCrrPu+VFXRDxrOg9b15A1gKqjA==",
|
||||
"requires": {
|
||||
"@sentry/core": "5.25.0",
|
||||
"@sentry/hub": "5.25.0",
|
||||
"@sentry/tracing": "5.25.0",
|
||||
"@sentry/types": "5.25.0",
|
||||
"@sentry/utils": "5.25.0",
|
||||
"@sentry/core": "6.13.2",
|
||||
"@sentry/hub": "6.13.2",
|
||||
"@sentry/tracing": "6.13.2",
|
||||
"@sentry/types": "6.13.2",
|
||||
"@sentry/utils": "6.13.2",
|
||||
"cookie": "^0.4.1",
|
||||
"https-proxy-agent": "^5.0.0",
|
||||
"lru_map": "^0.3.3",
|
||||
@ -2238,49 +2238,49 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"tslib": {
|
||||
"version": "1.14.0",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.0.tgz",
|
||||
"integrity": "sha512-+Zw5lu0D9tvBMjGP8LpvMb0u2WW2QV3y+D8mO6J+cNzCYIN4sVy43Bf9vl92nqFahutN0I8zHa7cc4vihIshnw=="
|
||||
"version": "1.14.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
|
||||
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"@sentry/tracing": {
|
||||
"version": "5.25.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/tracing/-/tracing-5.25.0.tgz",
|
||||
"integrity": "sha512-KcyHEGFpqSDubHrdWT/vF2hKkjw/ts6NpJ6tPDjBXUNz98BHdAyMKtLOFTCeJFply7/s5fyiAYu44M+M6IG3Bw==",
|
||||
"version": "6.13.2",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/tracing/-/tracing-6.13.2.tgz",
|
||||
"integrity": "sha512-bHJz+C/nd6biWTNcYAu91JeRilsvVgaye4POkdzWSmD0XoLWHVMrpCQobGpXe7onkp2noU3YQjhqgtBqPHtnpw==",
|
||||
"requires": {
|
||||
"@sentry/hub": "5.25.0",
|
||||
"@sentry/minimal": "5.25.0",
|
||||
"@sentry/types": "5.25.0",
|
||||
"@sentry/utils": "5.25.0",
|
||||
"@sentry/hub": "6.13.2",
|
||||
"@sentry/minimal": "6.13.2",
|
||||
"@sentry/types": "6.13.2",
|
||||
"@sentry/utils": "6.13.2",
|
||||
"tslib": "^1.9.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"tslib": {
|
||||
"version": "1.14.0",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.0.tgz",
|
||||
"integrity": "sha512-+Zw5lu0D9tvBMjGP8LpvMb0u2WW2QV3y+D8mO6J+cNzCYIN4sVy43Bf9vl92nqFahutN0I8zHa7cc4vihIshnw=="
|
||||
"version": "1.14.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
|
||||
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"@sentry/types": {
|
||||
"version": "5.25.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/types/-/types-5.25.0.tgz",
|
||||
"integrity": "sha512-8M4PREbcar+15wrtEqcwfcU33SS+2wBSIOd/NrJPXJPTYxi49VypCN1mZBDyWkaK+I+AuQwI3XlRPCfsId3D1A=="
|
||||
"version": "6.13.2",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/types/-/types-6.13.2.tgz",
|
||||
"integrity": "sha512-6WjGj/VjjN8LZDtqJH5ikeB1o39rO1gYS6anBxiS3d0sXNBb3Ux0pNNDFoBxQpOhmdDHXYS57MEptX9EV82gmg=="
|
||||
},
|
||||
"@sentry/utils": {
|
||||
"version": "5.25.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-5.25.0.tgz",
|
||||
"integrity": "sha512-Hz5spdIkMSRH5NR1YFOp5qbsY5Ud2lKhEQWlqxcVThMG5YNUc10aYv5ijL19v0YkrC2rqPjCRm7GrVtzOc7bXQ==",
|
||||
"version": "6.13.2",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-6.13.2.tgz",
|
||||
"integrity": "sha512-foF4PbxqPMWNbuqdXkdoOmKm3quu3PP7Q7j/0pXkri4DtCuvF/lKY92mbY0V9rHS/phCoj+3/Se5JvM2ymh2/w==",
|
||||
"requires": {
|
||||
"@sentry/types": "5.25.0",
|
||||
"@sentry/types": "6.13.2",
|
||||
"tslib": "^1.9.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"tslib": {
|
||||
"version": "1.14.0",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.0.tgz",
|
||||
"integrity": "sha512-+Zw5lu0D9tvBMjGP8LpvMb0u2WW2QV3y+D8mO6J+cNzCYIN4sVy43Bf9vl92nqFahutN0I8zHa7cc4vihIshnw=="
|
||||
"version": "1.14.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
|
||||
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -2385,6 +2385,17 @@
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"@types/cacheable-request": {
|
||||
"version": "6.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.2.tgz",
|
||||
"integrity": "sha512-B3xVo+dlKM6nnKTcmm5ZtY/OL8bOAOd2Olee9M1zft65ox50OzjEHW91sDiU9j6cvW8Ejg1/Qkf4xd2kugApUA==",
|
||||
"requires": {
|
||||
"@types/http-cache-semantics": "*",
|
||||
"@types/keyv": "*",
|
||||
"@types/node": "*",
|
||||
"@types/responselike": "*"
|
||||
}
|
||||
},
|
||||
"@types/caseless": {
|
||||
"version": "0.12.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/caseless/-/caseless-0.12.2.tgz",
|
||||
@ -2534,6 +2545,11 @@
|
||||
"integrity": "sha512-TikApqV8CkUsI1GGUgVydkJFrq9sYCBWv4fc/r3zvl6Oqe2YU1ASeWBrG5bw1D2XvS07YS3s05hCor/lEtIoYw==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/http-cache-semantics": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.1.tgz",
|
||||
"integrity": "sha512-SZs7ekbP8CN0txVG2xVRH6EgKmEm31BOxA07vkFaETzZz1xh+cbt8BcI0slpymvwhx5dlFnQG2rTlPVQn+iRPQ=="
|
||||
},
|
||||
"@types/http-proxy": {
|
||||
"version": "1.17.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.7.tgz",
|
||||
@ -2600,6 +2616,14 @@
|
||||
"resolved": "https://registry.npmjs.org/@types/jwt-decode/-/jwt-decode-2.2.1.tgz",
|
||||
"integrity": "sha512-aWw2YTtAdT7CskFyxEX2K21/zSDStuf/ikI3yBqmwpwJF0pS+/IX5DWv+1UFffZIbruP6cnT9/LAJV1gFwAT1A=="
|
||||
},
|
||||
"@types/keyv": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.2.tgz",
|
||||
"integrity": "sha512-/FvAK2p4jQOaJ6CGDHJTqZcUtbZe820qIeTg7o0Shg7drB4JHeL+V/dhSaly7NXx6u8eSee+r7coT+yuJEvDLg==",
|
||||
"requires": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"@types/klaw": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/klaw/-/klaw-3.0.2.tgz",
|
||||
@ -2615,14 +2639,14 @@
|
||||
"integrity": "sha512-7eQ2xYLLI/LsicL2nejW9Wyko3lcpN6O/z0ZLHrEQsg280zIdCv1t/0m6UtBjUHokCGBQ3gYTbHzDkZ1xOBwwg=="
|
||||
},
|
||||
"@types/lru-cache": {
|
||||
"version": "5.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/lru-cache/-/lru-cache-5.1.0.tgz",
|
||||
"integrity": "sha512-RaE0B+14ToE4l6UqdarKPnXwVDuigfFv+5j9Dze/Nqr23yyuqdNvzcZi3xB+3Agvi5R4EOgAksfv3lXX4vBt9w=="
|
||||
"version": "5.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/lru-cache/-/lru-cache-5.1.1.tgz",
|
||||
"integrity": "sha512-ssE3Vlrys7sdIzs5LOxCzTVMsU7i9oa/IaW92wF32JFb3CVczqOkru2xspuKczHEbG3nvmPY7IFqVmGGHdNbYw=="
|
||||
},
|
||||
"@types/memoizee": {
|
||||
"version": "0.4.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/memoizee/-/memoizee-0.4.5.tgz",
|
||||
"integrity": "sha512-+ZzZZ3+0a7/ajBPeAAD4+LxrBsCat0EFZQtO3o0rwpIeLmDmSaM8KF/oYPuFxeUFAMiHIHFcGucFnY/8S4Hszg=="
|
||||
"version": "0.4.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/memoizee/-/memoizee-0.4.6.tgz",
|
||||
"integrity": "sha512-qJezGqoi3pW9Pset2w1Gfv8jATvmHHHnpO9Dq8x8pJGyYIpiUZJqRU0NM7xenmN0AcXEe7vqshI8H98KeFLYcg=="
|
||||
},
|
||||
"@types/mime": {
|
||||
"version": "1.3.2",
|
||||
@ -2791,6 +2815,14 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"@types/responselike": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.0.tgz",
|
||||
"integrity": "sha512-85Y2BjiufFzaMIlvJDvTTB8Fxl2xfLo4HgmHzVBz08w4wDePCTjYw66PdrolO0kzli3yam/YCgRufyo1DdQVTA==",
|
||||
"requires": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"@types/rewire": {
|
||||
"version": "2.5.28",
|
||||
"resolved": "https://registry.npmjs.org/@types/rewire/-/rewire-2.5.28.tgz",
|
||||
@ -2905,9 +2937,9 @@
|
||||
}
|
||||
},
|
||||
"@types/uuid": {
|
||||
"version": "8.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-8.3.0.tgz",
|
||||
"integrity": "sha512-eQ9qFW/fhfGJF8WKHGEHZEyVWfZxrT+6CLIJGBcZPfxUh/+BnEj+UCGYMlr9qZuX/2AltsvwrGqp0LhEW8D0zQ=="
|
||||
"version": "8.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-8.3.1.tgz",
|
||||
"integrity": "sha512-Y2mHTRAbqfFkpjldbkHGY8JIzRN6XqYRliG8/24FcHm2D2PwW24fl5xMRTVGdrb7iMrwCaIEbLWerGIkXuFWVg=="
|
||||
},
|
||||
"@types/which": {
|
||||
"version": "2.0.1",
|
||||
@ -3061,9 +3093,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"agent-base": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.1.tgz",
|
||||
"integrity": "sha512-01q25QQDwLSsyfhrKbn8yuur+JNw0H+0Y4JiGIKd3z9aYk/w/2kxD/Upc+t2ZBBSUNff50VjPsSW2YxM8QYKVg==",
|
||||
"version": "6.0.2",
|
||||
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
|
||||
"integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==",
|
||||
"requires": {
|
||||
"debug": "4"
|
||||
}
|
||||
@ -3732,9 +3764,9 @@
|
||||
}
|
||||
},
|
||||
"balena-hup-action-utils": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/balena-hup-action-utils/-/balena-hup-action-utils-4.0.2.tgz",
|
||||
"integrity": "sha512-N2HVaqXodwR18HKnbOwJDzDZOLpQoPzH6MD4Ibs09Sb9OGBizgjAHiK4Qs20qX1wvztqm8sy8zWJ8nXEOFAplg==",
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/balena-hup-action-utils/-/balena-hup-action-utils-4.0.3.tgz",
|
||||
"integrity": "sha512-PdHMpSjaQriB4y4zmeAmm0Mxudencc/BVjI3jHVH3/SCZ6OqIIGlyFJU2tS0vsghED8PiEyQSjUdDCGzv7zUiQ==",
|
||||
"requires": {
|
||||
"balena-semver": "^2.0.0",
|
||||
"tslib": "^2.0.0"
|
||||
@ -3779,22 +3811,12 @@
|
||||
"balena-errors": "^4.2.1",
|
||||
"pinejs-client-core": "^6.9.0",
|
||||
"tslib": "^2.0.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"pinejs-client-core": {
|
||||
"version": "6.9.5",
|
||||
"resolved": "https://registry.npmjs.org/pinejs-client-core/-/pinejs-client-core-6.9.5.tgz",
|
||||
"integrity": "sha512-/QbmrR6IjlGZiM3JjZq9WtMdoR5UWW4zI+mCElm6Ez6kZiBYnWbHEDaVpyfEAj3mmSf6bj9ezQTxU+eLnzC1OQ==",
|
||||
"requires": {
|
||||
"@balena/es-version": "^1.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"balena-preload": {
|
||||
"version": "10.4.20",
|
||||
"resolved": "https://registry.npmjs.org/balena-preload/-/balena-preload-10.4.20.tgz",
|
||||
"integrity": "sha512-uKbccD1oh7BQ9XUMXYQvGklfws1v9+oaWkYxE0eheh+0GcHP+CVWr/TIUx+P3eplh7cAfeZAxw0wx0jp8H1Ttg==",
|
||||
"version": "10.5.0",
|
||||
"resolved": "https://registry.npmjs.org/balena-preload/-/balena-preload-10.5.0.tgz",
|
||||
"integrity": "sha512-tgnTyOSOLB3HxIqlR1NFrTsy1eiiew5Vzmplb82/eZc/vJTrOqal2tFNn6aFay6UQ8+OASUJwANC99zBu1e8mQ==",
|
||||
"requires": {
|
||||
"archiver": "^3.1.1",
|
||||
"balena-sdk": "^15.44.0",
|
||||
@ -3827,32 +3849,6 @@
|
||||
"zip-stream": "^2.1.2"
|
||||
}
|
||||
},
|
||||
"balena-sdk": {
|
||||
"version": "15.45.0",
|
||||
"resolved": "https://registry.npmjs.org/balena-sdk/-/balena-sdk-15.45.0.tgz",
|
||||
"integrity": "sha512-lfz6x8yL7nV+zHTC2+w30Dtv7I5rz8IxoO6Ifjb4/ewkhz6LyX1/UhH/a4k83oaQMXIuoYnmwWE6iVx/zl9xXQ==",
|
||||
"requires": {
|
||||
"@balena/es-version": "^1.0.0",
|
||||
"@types/lodash": "^4.14.168",
|
||||
"@types/memoizee": "^0.4.5",
|
||||
"@types/node": "^10.17.55",
|
||||
"abortcontroller-polyfill": "^1.7.1",
|
||||
"balena-auth": "^4.1.0",
|
||||
"balena-errors": "^4.7.1",
|
||||
"balena-hup-action-utils": "~4.0.2",
|
||||
"balena-pine": "^12.4.0",
|
||||
"balena-register-device": "^7.1.0",
|
||||
"balena-request": "^11.4.0",
|
||||
"balena-semver": "^2.3.0",
|
||||
"balena-settings-client": "^4.0.6",
|
||||
"lodash": "^4.17.21",
|
||||
"memoizee": "^0.4.15",
|
||||
"moment": "^2.29.1",
|
||||
"ndjson": "^2.0.0",
|
||||
"semver": "^7.3.4",
|
||||
"tslib": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"compress-commons": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-2.1.1.tgz",
|
||||
@ -3938,9 +3934,9 @@
|
||||
}
|
||||
},
|
||||
"tslib": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz",
|
||||
"integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg=="
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz",
|
||||
"integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw=="
|
||||
},
|
||||
"zip-stream": {
|
||||
"version": "2.1.3",
|
||||
@ -3966,16 +3962,16 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"tslib": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.2.0.tgz",
|
||||
"integrity": "sha512-gS9GVHRU+RGn5KQM2rllAlR3dU6m7AcpJKdtH8gFvQiC4Otgk98XnmMU+nZenHt/+VhnBPWwgrJsyrdcw6i23w=="
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz",
|
||||
"integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"balena-release": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/balena-release/-/balena-release-3.0.0.tgz",
|
||||
"integrity": "sha512-LYgPGBUnqJY+ajhTWE2BizyaRNxitxPSIZp4xGWDHbpVHmwKaV4p3d6nw1Hf9kEHP4dLELJvnNXq2EKV2IpTFA==",
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/balena-release/-/balena-release-3.2.0.tgz",
|
||||
"integrity": "sha512-jwmAjIZCJ5I46/yQNN+dA73RWlre0+jBVmo2QeJl1pK83obTLyifJeWNVf5irzP8KFE7WQzo9ICK1cCpLtygFA==",
|
||||
"requires": {
|
||||
"@types/bluebird": "^3.5.18",
|
||||
"@types/node": "^8.0.55",
|
||||
@ -3987,16 +3983,16 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/node": {
|
||||
"version": "8.10.62",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-8.10.62.tgz",
|
||||
"integrity": "sha512-76fupxOYVxk36kb7O/6KtrAPZ9jnSK3+qisAX4tQMEuGNdlvl7ycwatlHqjoE6jHfVtXFM3pCrCixZOidc5cuw=="
|
||||
"version": "8.10.66",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-8.10.66.tgz",
|
||||
"integrity": "sha512-tktOkFUA4kXx2hhhrB8bIFb5TbwzS4uOhKEmwiD+NoiL0qtP2OQ9mFldbgD4dV1djrlBYP6eBuQZiWjuHUpqFw=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"balena-request": {
|
||||
"version": "11.4.0",
|
||||
"resolved": "https://registry.npmjs.org/balena-request/-/balena-request-11.4.0.tgz",
|
||||
"integrity": "sha512-wfPaWX/+NgT2xNplQqA8oCNLJXG6eLMbf9IOX8T4ZX+nqBoA9bydoIRLunGExMNfUWpxApvBh5ls8fJOd9VTjQ==",
|
||||
"version": "11.4.2",
|
||||
"resolved": "https://registry.npmjs.org/balena-request/-/balena-request-11.4.2.tgz",
|
||||
"integrity": "sha512-J4SrFBUR4AB2Y3afsX2QAMZ7H/zjysXjOyEhEqvjNTsBfe5ReCmf17vzRQ8q2URCm00nUhQgfQtlJUq6miB1/g==",
|
||||
"requires": {
|
||||
"@balena/node-web-streams": "^0.2.3",
|
||||
"balena-errors": "^4.7.1",
|
||||
@ -4008,9 +4004,9 @@
|
||||
}
|
||||
},
|
||||
"balena-sdk": {
|
||||
"version": "15.36.0",
|
||||
"resolved": "https://registry.npmjs.org/balena-sdk/-/balena-sdk-15.36.0.tgz",
|
||||
"integrity": "sha512-h55dSJpZ8XCJAwvbfinCuGPQXfSIBg/ftkpd6ObV+WQ/iN7DIpBKG0QVPdWoK91Ws0Ie/GzmW4IFvgtS/Yf3hQ==",
|
||||
"version": "15.51.1",
|
||||
"resolved": "https://registry.npmjs.org/balena-sdk/-/balena-sdk-15.51.1.tgz",
|
||||
"integrity": "sha512-EMCQruytqyvpfxvjq9Zd/wWnnOAIl/Wd1majqv6hqa+z104UUTjnRCQaUji4mo8YtrFLn7aUUZFFHIKiv/3sTg==",
|
||||
"requires": {
|
||||
"@balena/es-version": "^1.0.0",
|
||||
"@types/lodash": "^4.14.168",
|
||||
@ -4022,7 +4018,7 @@
|
||||
"balena-hup-action-utils": "~4.0.2",
|
||||
"balena-pine": "^12.4.0",
|
||||
"balena-register-device": "^7.1.0",
|
||||
"balena-request": "^11.4.0",
|
||||
"balena-request": "^11.4.2",
|
||||
"balena-semver": "^2.3.0",
|
||||
"balena-settings-client": "^4.0.6",
|
||||
"lodash": "^4.17.21",
|
||||
@ -4047,9 +4043,9 @@
|
||||
}
|
||||
},
|
||||
"tslib": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.2.0.tgz",
|
||||
"integrity": "sha512-gS9GVHRU+RGn5KQM2rllAlR3dU6m7AcpJKdtH8gFvQiC4Otgk98XnmMU+nZenHt/+VhnBPWwgrJsyrdcw6i23w=="
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz",
|
||||
"integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw=="
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -4065,9 +4061,9 @@
|
||||
}
|
||||
},
|
||||
"balena-settings-client": {
|
||||
"version": "4.0.6",
|
||||
"resolved": "https://registry.npmjs.org/balena-settings-client/-/balena-settings-client-4.0.6.tgz",
|
||||
"integrity": "sha512-bB14Zvg1N6t7XXPJqZs48SajgTuk2WTMm2AnxcOfoIQ2d/Lh0RsEGxD9toF2v+WhF2Ip4u7ko5tKlCr2kFddXA==",
|
||||
"version": "4.0.7",
|
||||
"resolved": "https://registry.npmjs.org/balena-settings-client/-/balena-settings-client-4.0.7.tgz",
|
||||
"integrity": "sha512-1ncEgufbAbzcfcffsTpi20asNdsOEZxACiQhv8naQp1mgw6INe/0FvSNX6St+XlXtuk1FqCnYNINGIjMoStOrA==",
|
||||
"requires": {
|
||||
"@resin.io/types-hidepath": "1.0.1",
|
||||
"@resin.io/types-home-or-tmp": "3.0.0",
|
||||
@ -4597,6 +4593,11 @@
|
||||
"unset-value": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"cacheable-lookup": {
|
||||
"version": "5.0.4",
|
||||
"resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz",
|
||||
"integrity": "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA=="
|
||||
},
|
||||
"cacheable-request": {
|
||||
"version": "6.1.0",
|
||||
"resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-6.1.0.tgz",
|
||||
@ -6496,6 +6497,14 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"dockerfile-ast": {
|
||||
"version": "0.2.1",
|
||||
"resolved": "https://registry.npmjs.org/dockerfile-ast/-/dockerfile-ast-0.2.1.tgz",
|
||||
"integrity": "sha512-ut04CVM1G6zIITTcYPDIXhPZk9mCa21m4dfW8FcDDGxwgTQhYyHDu6U7M8klZ7QsjqVcJhryKi+TGOX6bjgKdQ==",
|
||||
"requires": {
|
||||
"vscode-languageserver-types": "^3.16.0"
|
||||
}
|
||||
},
|
||||
"dockerfile-template": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/dockerfile-template/-/dockerfile-template-0.2.0.tgz",
|
||||
@ -6899,19 +6908,6 @@
|
||||
"es6-symbol": "^3.1.1"
|
||||
}
|
||||
},
|
||||
"es6-promise": {
|
||||
"version": "4.2.8",
|
||||
"resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz",
|
||||
"integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w=="
|
||||
},
|
||||
"es6-promisify": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/es6-promisify/-/es6-promisify-5.0.0.tgz",
|
||||
"integrity": "sha1-UQnWLz5W6pZ8S2NQWu8IKRyKUgM=",
|
||||
"requires": {
|
||||
"es6-promise": "^4.0.3"
|
||||
}
|
||||
},
|
||||
"es6-symbol": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.3.tgz",
|
||||
@ -7778,7 +7774,6 @@
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/ffi-napi/-/ffi-napi-4.0.3.tgz",
|
||||
"integrity": "sha512-PMdLCIvDY9mS32RxZ0XGb95sonPRal8aqRhLbeEtWKZTe2A87qRFG9HjOhvG8EX2UmQw5XNRMIOT+1MYlWmdeg==",
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"debug": "^4.1.1",
|
||||
"get-uv-event-loop-napi-h": "^1.0.5",
|
||||
@ -7789,10 +7784,9 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"node-addon-api": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-3.1.0.tgz",
|
||||
"integrity": "sha512-flmrDNB06LIl5lywUz7YlNGZH/5p0M7W28k8hzd9Lshtdh1wshD2Y+U4h9LD6KObOy1f+fEVdgprPrEymjM5uw==",
|
||||
"optional": true
|
||||
"version": "3.2.1",
|
||||
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-3.2.1.tgz",
|
||||
"integrity": "sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A=="
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -8203,9 +8197,9 @@
|
||||
"integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ="
|
||||
},
|
||||
"fp-ts": {
|
||||
"version": "2.10.5",
|
||||
"resolved": "https://registry.npmjs.org/fp-ts/-/fp-ts-2.10.5.tgz",
|
||||
"integrity": "sha512-X2KfTIV0cxIk3d7/2Pvp/pxL/xr2MV1WooyEzKtTWYSc1+52VF4YzjBTXqeOlSiZsPCxIBpDGfT9Dyo7WEY0DQ=="
|
||||
"version": "2.11.3",
|
||||
"resolved": "https://registry.npmjs.org/fp-ts/-/fp-ts-2.11.3.tgz",
|
||||
"integrity": "sha512-qHI5iaVSFNFmdl6yDensWfFMk32iafAINCnqx8m486DV1+Jht/bTnA9CyahL+Xm7h2y3erinviVBIAWvv5bPYw=="
|
||||
},
|
||||
"fragment-cache": {
|
||||
"version": "0.2.1",
|
||||
@ -8433,14 +8427,12 @@
|
||||
"get-symbol-from-current-process-h": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/get-symbol-from-current-process-h/-/get-symbol-from-current-process-h-1.0.2.tgz",
|
||||
"integrity": "sha512-syloC6fsCt62ELLrr1VKBM1ggOpMdetX9hTrdW77UQdcApPHLmf7CI7OKcN1c9kYuNxKcDe4iJ4FY9sX3aw2xw==",
|
||||
"optional": true
|
||||
"integrity": "sha512-syloC6fsCt62ELLrr1VKBM1ggOpMdetX9hTrdW77UQdcApPHLmf7CI7OKcN1c9kYuNxKcDe4iJ4FY9sX3aw2xw=="
|
||||
},
|
||||
"get-uv-event-loop-napi-h": {
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/get-uv-event-loop-napi-h/-/get-uv-event-loop-napi-h-1.0.6.tgz",
|
||||
"integrity": "sha512-t5c9VNR84nRoF+eLiz6wFrEp1SE2Acg0wS+Ysa2zF0eROes+LzOfuTaVHxGy8AbS8rq7FHEJzjnCZo1BupwdJg==",
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"get-symbol-from-current-process-h": "^1.0.1"
|
||||
}
|
||||
@ -8465,6 +8457,31 @@
|
||||
"requires": {
|
||||
"got": "^6.2.0",
|
||||
"is-plain-obj": "^1.1.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"get-stream": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz",
|
||||
"integrity": "sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ="
|
||||
},
|
||||
"got": {
|
||||
"version": "6.7.1",
|
||||
"resolved": "https://registry.npmjs.org/got/-/got-6.7.1.tgz",
|
||||
"integrity": "sha1-JAzQV4WpoY5WHcG0S0HHY+8ejbA=",
|
||||
"requires": {
|
||||
"create-error-class": "^3.0.0",
|
||||
"duplexer3": "^0.1.4",
|
||||
"get-stream": "^3.0.0",
|
||||
"is-redirect": "^1.0.0",
|
||||
"is-retry-allowed": "^1.0.0",
|
||||
"is-stream": "^1.0.0",
|
||||
"lowercase-keys": "^1.0.0",
|
||||
"safe-buffer": "^5.0.1",
|
||||
"timed-out": "^4.0.0",
|
||||
"unzip-response": "^2.0.1",
|
||||
"url-parse-lax": "^1.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"ghauth": {
|
||||
@ -8941,27 +8958,111 @@
|
||||
}
|
||||
},
|
||||
"got": {
|
||||
"version": "6.7.1",
|
||||
"resolved": "https://registry.npmjs.org/got/-/got-6.7.1.tgz",
|
||||
"integrity": "sha1-JAzQV4WpoY5WHcG0S0HHY+8ejbA=",
|
||||
"version": "11.8.2",
|
||||
"resolved": "https://registry.npmjs.org/got/-/got-11.8.2.tgz",
|
||||
"integrity": "sha512-D0QywKgIe30ODs+fm8wMZiAcZjypcCodPNuMz5H9Mny7RJ+IjJ10BdmGW7OM7fHXP+O7r6ZwapQ/YQmMSvB0UQ==",
|
||||
"requires": {
|
||||
"create-error-class": "^3.0.0",
|
||||
"duplexer3": "^0.1.4",
|
||||
"get-stream": "^3.0.0",
|
||||
"is-redirect": "^1.0.0",
|
||||
"is-retry-allowed": "^1.0.0",
|
||||
"is-stream": "^1.0.0",
|
||||
"lowercase-keys": "^1.0.0",
|
||||
"safe-buffer": "^5.0.1",
|
||||
"timed-out": "^4.0.0",
|
||||
"unzip-response": "^2.0.1",
|
||||
"url-parse-lax": "^1.0.0"
|
||||
"@sindresorhus/is": "^4.0.0",
|
||||
"@szmarczak/http-timer": "^4.0.5",
|
||||
"@types/cacheable-request": "^6.0.1",
|
||||
"@types/responselike": "^1.0.0",
|
||||
"cacheable-lookup": "^5.0.3",
|
||||
"cacheable-request": "^7.0.1",
|
||||
"decompress-response": "^6.0.0",
|
||||
"http2-wrapper": "^1.0.0-beta.5.2",
|
||||
"lowercase-keys": "^2.0.0",
|
||||
"p-cancelable": "^2.0.0",
|
||||
"responselike": "^2.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@sindresorhus/is": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.0.1.tgz",
|
||||
"integrity": "sha512-Qm9hBEBu18wt1PO2flE7LPb30BHMQt1eQgbV76YntdNk73XZGpn3izvGTYxbGgzXKgbCjiia0uxTd3aTNQrY/g=="
|
||||
},
|
||||
"@szmarczak/http-timer": {
|
||||
"version": "4.0.6",
|
||||
"resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz",
|
||||
"integrity": "sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==",
|
||||
"requires": {
|
||||
"defer-to-connect": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"cacheable-request": {
|
||||
"version": "7.0.2",
|
||||
"resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-7.0.2.tgz",
|
||||
"integrity": "sha512-pouW8/FmiPQbuGpkXQ9BAPv/Mo5xDGANgSNXzTzJ8DrKGuXOssM4wIQRjfanNRh3Yu5cfYPvcorqbhg2KIJtew==",
|
||||
"requires": {
|
||||
"clone-response": "^1.0.2",
|
||||
"get-stream": "^5.1.0",
|
||||
"http-cache-semantics": "^4.0.0",
|
||||
"keyv": "^4.0.0",
|
||||
"lowercase-keys": "^2.0.0",
|
||||
"normalize-url": "^6.0.1",
|
||||
"responselike": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"decompress-response": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz",
|
||||
"integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==",
|
||||
"requires": {
|
||||
"mimic-response": "^3.1.0"
|
||||
}
|
||||
},
|
||||
"defer-to-connect": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz",
|
||||
"integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg=="
|
||||
},
|
||||
"get-stream": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz",
|
||||
"integrity": "sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ="
|
||||
"version": "5.2.0",
|
||||
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz",
|
||||
"integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==",
|
||||
"requires": {
|
||||
"pump": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"json-buffer": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
|
||||
"integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="
|
||||
},
|
||||
"keyv": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.0.3.tgz",
|
||||
"integrity": "sha512-zdGa2TOpSZPq5mU6iowDARnMBZgtCqJ11dJROFi6tg6kTn4nuUdU09lFyLFSaHrWqpIJ+EBq4E8/Dc0Vx5vLdA==",
|
||||
"requires": {
|
||||
"json-buffer": "3.0.1"
|
||||
}
|
||||
},
|
||||
"lowercase-keys": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz",
|
||||
"integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA=="
|
||||
},
|
||||
"mimic-response": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz",
|
||||
"integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ=="
|
||||
},
|
||||
"normalize-url": {
|
||||
"version": "6.1.0",
|
||||
"resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz",
|
||||
"integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A=="
|
||||
},
|
||||
"p-cancelable": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.1.1.tgz",
|
||||
"integrity": "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg=="
|
||||
},
|
||||
"responselike": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/responselike/-/responselike-2.0.0.tgz",
|
||||
"integrity": "sha512-xH48u3FTB9VsZw7R+vvgaKeLKzT6jOogbQhEe/jewwnZgzPcnyWui2Av6JpoYZF/91uueC+lqhWqeURw5/qhCw==",
|
||||
"requires": {
|
||||
"lowercase-keys": "^2.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -9424,6 +9525,15 @@
|
||||
"sshpk": "^1.7.0"
|
||||
}
|
||||
},
|
||||
"http2-wrapper": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.3.tgz",
|
||||
"integrity": "sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==",
|
||||
"requires": {
|
||||
"quick-lru": "^5.1.1",
|
||||
"resolve-alpn": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"https-proxy-agent": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz",
|
||||
@ -11870,41 +11980,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"mixpanel": {
|
||||
"version": "0.10.3",
|
||||
"resolved": "https://registry.npmjs.org/mixpanel/-/mixpanel-0.10.3.tgz",
|
||||
"integrity": "sha512-wIYr5o+1XSzJ80o3QED35K/yfPAKi5FigZXTSfcs4vltfeKbilIjNgwxdno7LrqzhjoSjmIyDWkI7D3lr7TwDw==",
|
||||
"requires": {
|
||||
"https-proxy-agent": "3.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"agent-base": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-4.3.0.tgz",
|
||||
"integrity": "sha512-salcGninV0nPrwpGNn4VTXBb1SOuXQBiqbrNXoeizJsHrsL6ERFM2Ne3JUSBWRE6aeNJI2ROP/WEEIDUiDe3cg==",
|
||||
"requires": {
|
||||
"es6-promisify": "^5.0.0"
|
||||
}
|
||||
},
|
||||
"debug": {
|
||||
"version": "3.2.6",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz",
|
||||
"integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==",
|
||||
"requires": {
|
||||
"ms": "^2.1.1"
|
||||
}
|
||||
},
|
||||
"https-proxy-agent": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-3.0.0.tgz",
|
||||
"integrity": "sha512-y4jAxNEihqvBI5F3SaO2rtsjIOnnNA8sEbuiP+UhJZJHeM2NRm6c09ax2tgqme+SgUUvjao2fJXF4h3D6Cb2HQ==",
|
||||
"requires": {
|
||||
"agent-base": "^4.3.0",
|
||||
"debug": "^3.1.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"mkdirp": {
|
||||
"version": "0.5.5",
|
||||
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz",
|
||||
@ -12471,10 +12546,9 @@
|
||||
}
|
||||
},
|
||||
"net-keepalive": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/net-keepalive/-/net-keepalive-2.0.3.tgz",
|
||||
"integrity": "sha512-VNWUXuLR0tUkZT2VVanJ5VqlBTJL8O5tGVsgq/FrvhHO7vjVC8gfU0ogRL+hxnfYuzSkMaiTHZfdIqwu7Q4zXA==",
|
||||
"optional": true,
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/net-keepalive/-/net-keepalive-3.0.0.tgz",
|
||||
"integrity": "sha512-wfDa7VPeSltY5aIQcujS7AiWnO2JHJCpO3is4nwQ7kFYs4YMpzDNMwiuILPkWwgMbPMSHzO7O1tuL8rC0SP3ag==",
|
||||
"requires": {
|
||||
"ffi-napi": "^4.0.1",
|
||||
"ref-napi": "^3.0.0"
|
||||
@ -12565,8 +12639,7 @@
|
||||
"node-gyp-build": {
|
||||
"version": "4.2.3",
|
||||
"resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.2.3.tgz",
|
||||
"integrity": "sha512-MN6ZpzmfNCRM+3t57PTJHgHyw/h4OWnZ6mR8P5j/uZtqQr46RRuDE/P+g3n0YR/AiYXeWixZZzaip77gdICfRg==",
|
||||
"optional": true
|
||||
"integrity": "sha512-MN6ZpzmfNCRM+3t57PTJHgHyw/h4OWnZ6mR8P5j/uZtqQr46RRuDE/P+g3n0YR/AiYXeWixZZzaip77gdICfRg=="
|
||||
},
|
||||
"node-pre-gyp": {
|
||||
"version": "0.14.0",
|
||||
@ -13806,24 +13879,24 @@
|
||||
"integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY="
|
||||
},
|
||||
"pinejs-client-core": {
|
||||
"version": "6.7.3",
|
||||
"resolved": "https://registry.npmjs.org/pinejs-client-core/-/pinejs-client-core-6.7.3.tgz",
|
||||
"integrity": "sha512-VXX/EpbDC/LiEPix/S9gENsaruoa2N0GHzmEbf9jYZ7qhwGNJjGlnQXQ1AnE/cjHYgHOo2RnMYKJ+iBakFgnWQ==",
|
||||
"version": "6.9.6",
|
||||
"resolved": "https://registry.npmjs.org/pinejs-client-core/-/pinejs-client-core-6.9.6.tgz",
|
||||
"integrity": "sha512-XUfHeYxT65PIxaV2SWZ7o/2nBUpTg9NaAcrfIRHgMkWQVlcUnh5EguHXoo2nKA4ocKds1fxIheuT29JqEg9SWQ==",
|
||||
"requires": {
|
||||
"@balena/es-version": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"pinejs-client-request": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/pinejs-client-request/-/pinejs-client-request-7.2.0.tgz",
|
||||
"integrity": "sha512-rzUbSc3AkxHKlEz3TeJ5txABoxVsrYGSLjcv9uS3A7oVuObF8vD/CUFredJ/5m3hnvSRRIpRl0nPyugRAqTS+g==",
|
||||
"version": "7.3.3",
|
||||
"resolved": "https://registry.npmjs.org/pinejs-client-request/-/pinejs-client-request-7.3.3.tgz",
|
||||
"integrity": "sha512-HmJfI/yvRB5mrwPedSIMhgcdWco1g4BfGa3bIwEoncOAi808y9FhfPrbcBe1ZjWGtGZTZRE020LpkMaIyigzBg==",
|
||||
"requires": {
|
||||
"@types/lodash": "^4.14.159",
|
||||
"@types/lodash": "^4.14.168",
|
||||
"@types/lru-cache": "^5.1.0",
|
||||
"@types/request": "^2.48.5",
|
||||
"lodash": "^4.17.19",
|
||||
"lodash": "^4.17.21",
|
||||
"lru-cache": "^6.0.0",
|
||||
"pinejs-client-core": "^6.6.1",
|
||||
"pinejs-client-core": "^6.9.5",
|
||||
"request": "^2.88.2",
|
||||
"typed-error": "^3.2.1"
|
||||
}
|
||||
@ -14712,6 +14785,11 @@
|
||||
"resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz",
|
||||
"integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA="
|
||||
},
|
||||
"quick-lru": {
|
||||
"version": "5.1.1",
|
||||
"resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz",
|
||||
"integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA=="
|
||||
},
|
||||
"randombytes": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
|
||||
@ -15063,22 +15141,20 @@
|
||||
}
|
||||
},
|
||||
"ref-napi": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/ref-napi/-/ref-napi-3.0.2.tgz",
|
||||
"integrity": "sha512-5YE0XrvWteoTr5DR2sEqxefL06aml7c6qS7hGv3u27do4HlGQphwvB+zD1NYep9utMKScvwOZsSs9EPYdGBVsg==",
|
||||
"optional": true,
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/ref-napi/-/ref-napi-3.0.3.tgz",
|
||||
"integrity": "sha512-LiMq/XDGcgodTYOMppikEtJelWsKQERbLQsYm0IOOnzhwE9xYZC7x8txNnFC9wJNOkPferQI4vD4ZkC0mDyrOA==",
|
||||
"requires": {
|
||||
"debug": "^4.1.1",
|
||||
"get-symbol-from-current-process-h": "^1.0.2",
|
||||
"node-addon-api": "^2.0.0",
|
||||
"node-addon-api": "^3.0.0",
|
||||
"node-gyp-build": "^4.2.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"node-addon-api": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-2.0.2.tgz",
|
||||
"integrity": "sha512-Ntyt4AIXyaLIuMHF6IOoTakB3K+RWxwtsHNRxllEoA6vPwP9o4866g6YWDLUdnucilZhmkxiHwHr11gAENw+QA==",
|
||||
"optional": true
|
||||
"version": "3.2.1",
|
||||
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-3.2.1.tgz",
|
||||
"integrity": "sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A=="
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -15086,7 +15162,6 @@
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/ref-struct-di/-/ref-struct-di-1.1.1.tgz",
|
||||
"integrity": "sha512-2Xyn/0Qgz89VT+++WP0sTosdm9oeowLP23wRJYhG4BFdMUrLj3jhwHZNEytYNYgtPKLNTP3KJX4HEgBvM1/Y2g==",
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"debug": "^3.1.0"
|
||||
},
|
||||
@ -15095,7 +15170,6 @@
|
||||
"version": "3.2.7",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
|
||||
"integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"ms": "^2.1.1"
|
||||
}
|
||||
@ -15366,9 +15440,9 @@
|
||||
}
|
||||
},
|
||||
"resin-compose-parse": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/resin-compose-parse/-/resin-compose-parse-2.1.3.tgz",
|
||||
"integrity": "sha512-X5WQo+OHoPe+FV8JliGzSIL4glLX0PPFvtnopppYef1UqKcJm+GHaiEZBOj3C7vIEDqQrsNrKXY/BpadlOFiWA==",
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/resin-compose-parse/-/resin-compose-parse-2.3.0.tgz",
|
||||
"integrity": "sha512-9vE+ascGEXuyTYjLmhwmVOYNkws99Cq7/RtUrgT8VCYJRuhqZMpGMOJPrjEIF6PvsBY7B6+u32x1HPCR4gWHBQ==",
|
||||
"requires": {
|
||||
"@types/lodash": "^4.14.86",
|
||||
"@types/node": "^8.0.55",
|
||||
@ -15442,9 +15516,12 @@
|
||||
}
|
||||
},
|
||||
"@types/klaw": {
|
||||
"version": "1.3.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/klaw/-/klaw-1.3.5.tgz",
|
||||
"integrity": "sha512-KZfv4ea6bEbdQhfwpxtDuTPO2mHAAXMQqPOZyS4MgNyCymKoLHp0FVzzYq3H2zCeIotN4h1453TahLCCm8rf2w=="
|
||||
"version": "1.3.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/klaw/-/klaw-1.3.6.tgz",
|
||||
"integrity": "sha512-4pr2RxwhfsLxFYa4Ip8JxrdXIvPX7fAqyBh9ofZPedMwf8M5CIcSQskqvX6/5Y/zpCBHtuC3218t8H+XJsg5FA==",
|
||||
"requires": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"bl": {
|
||||
"version": "1.2.3",
|
||||
@ -15641,13 +15718,14 @@
|
||||
}
|
||||
},
|
||||
"resin-multibuild": {
|
||||
"version": "4.11.0",
|
||||
"resolved": "https://registry.npmjs.org/resin-multibuild/-/resin-multibuild-4.11.0.tgz",
|
||||
"integrity": "sha512-rIYV9GDNuI8pU9N+wGdVRIOGAnw1BFdbyt3BkvERFxbf+b/e7jpBjHkbK8VPQdRMlKPyu137ZxQlR3z7EivJBg==",
|
||||
"version": "4.12.2",
|
||||
"resolved": "https://registry.npmjs.org/resin-multibuild/-/resin-multibuild-4.12.2.tgz",
|
||||
"integrity": "sha512-FkRqGEM588wA6v03pQbodPqWQdAs6aMh+GWvYQBz5IxqSVecn4FLHaRE0pF6VFKtjf/XBuPw7dtqiFzH+NIz5g==",
|
||||
"requires": {
|
||||
"ajv": "^6.12.3",
|
||||
"bluebird": "^3.7.2",
|
||||
"docker-progress": "^5.0.0",
|
||||
"dockerfile-ast": "^0.2.1",
|
||||
"dockerfile-template": "^0.2.0",
|
||||
"dockerode": "^2.5.8",
|
||||
"fp-ts": "^2.8.1",
|
||||
@ -15887,6 +15965,11 @@
|
||||
"path-parse": "^1.0.6"
|
||||
}
|
||||
},
|
||||
"resolve-alpn": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.0.tgz",
|
||||
"integrity": "sha512-e4FNQs+9cINYMO5NMFc6kOUCdohjqFPSgMuwuZAOUWqrfWsen+Yjy5qZFkV5K7VO7tFSLKcUL97olkED7sCBHA=="
|
||||
},
|
||||
"resolve-dir": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/resolve-dir/-/resolve-dir-1.0.1.tgz",
|
||||
@ -16383,13 +16466,13 @@
|
||||
"dev": true
|
||||
},
|
||||
"sinon": {
|
||||
"version": "11.1.1",
|
||||
"resolved": "https://registry.npmjs.org/sinon/-/sinon-11.1.1.tgz",
|
||||
"integrity": "sha512-ZSSmlkSyhUWbkF01Z9tEbxZLF/5tRC9eojCdFh33gtQaP7ITQVaMWQHGuFM7Cuf/KEfihuh1tTl3/ABju3AQMg==",
|
||||
"version": "11.1.2",
|
||||
"resolved": "https://registry.npmjs.org/sinon/-/sinon-11.1.2.tgz",
|
||||
"integrity": "sha512-59237HChms4kg7/sXhiRcUzdSkKuydDeTiamT/jesUVHshBgL8XAmhgFo0GfK6RruMDM/iRSij1EybmMog9cJw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@sinonjs/commons": "^1.8.3",
|
||||
"@sinonjs/fake-timers": "^7.1.0",
|
||||
"@sinonjs/fake-timers": "^7.1.2",
|
||||
"@sinonjs/samsam": "^6.0.2",
|
||||
"diff": "^5.0.0",
|
||||
"nise": "^5.1.0",
|
||||
@ -18408,6 +18491,11 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"vscode-languageserver-types": {
|
||||
"version": "3.16.0",
|
||||
"resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.16.0.tgz",
|
||||
"integrity": "sha512-k8luDIWJWyenLc5ToFQQMaSrqCHiLwyKPHKPQZ5zz21vM+vIVUSvsRpcbiECH4WR88K2XZqc4ScRcZ7nk/jbeA=="
|
||||
},
|
||||
"wcwidth": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz",
|
||||
@ -18417,9 +18505,9 @@
|
||||
}
|
||||
},
|
||||
"web-streams-polyfill": {
|
||||
"version": "1.3.2",
|
||||
"resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-1.3.2.tgz",
|
||||
"integrity": "sha1-NxkkXpCSgtk5Z4JfRLzVUOnAOZU="
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.1.1.tgz",
|
||||
"integrity": "sha512-Czi3fG883e96T4DLEPRvufrF2ydhOOW1+1a6c3gNjH2aIh50DNFBdfwh2AKoOf1rXvpvavAoA11Qdq9+BKjE0Q=="
|
||||
},
|
||||
"which": {
|
||||
"version": "2.0.2",
|
||||
|
26
package.json
26
package.json
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "balena-cli",
|
||||
"version": "12.45.0",
|
||||
"version": "12.50.2",
|
||||
"description": "The official balena Command Line Interface",
|
||||
"main": "./build/app.js",
|
||||
"homepage": "https://github.com/balena-io/balena-cli",
|
||||
@ -49,6 +49,7 @@
|
||||
"postinstall": "node patches/apply-patches.js",
|
||||
"prebuild": "rimraf build/ build-bin/",
|
||||
"build": "npm run build:src && npm run catch-uncommitted",
|
||||
"build:t": "npm run lint && npm run build:fast && npm run build:test",
|
||||
"build:src": "npm run lint && npm run build:fast && npm run build:test && npm run build:doc && npm run build:completion",
|
||||
"build:fast": "gulp pages && tsc && npx oclif-dev manifest",
|
||||
"build:test": "tsc -P ./tsconfig.dev.json --noEmit && tsc -P ./tsconfig.js.json --noEmit",
|
||||
@ -65,6 +66,7 @@
|
||||
"test:standalone": "npm run build:standalone && npm run test:standalone:fast",
|
||||
"test:standalone:fast": "cross-env BALENA_CLI_TEST_TYPE=standalone mocha --config .mocharc-standalone.js",
|
||||
"test:fast": "npm run build:fast && npm run test:source",
|
||||
"test:debug": "cross-env BALENA_CLI_TEST_TYPE=source mocha --inspect-brk=0.0.0.0",
|
||||
"test:only": "npm run build:fast && cross-env BALENA_CLI_TEST_TYPE=source mocha \"tests/**/${npm_config_test}.spec.ts\"",
|
||||
"catch-uncommitted": "ts-node --transpile-only automation/run.ts catch-uncommitted",
|
||||
"ci": "npm run test && npm run catch-uncommitted",
|
||||
@ -185,7 +187,7 @@
|
||||
"publish-release": "^1.6.1",
|
||||
"rewire": "^5.0.0",
|
||||
"simple-git": "^2.40.0",
|
||||
"sinon": "^11.1.1",
|
||||
"sinon": "^11.1.2",
|
||||
"ts-node": "^10.0.0",
|
||||
"typescript": "^4.3.5"
|
||||
},
|
||||
@ -194,7 +196,7 @@
|
||||
"@balena/es-version": "^1.0.0",
|
||||
"@oclif/command": "^1.8.0",
|
||||
"@resin.io/valid-email": "^0.1.0",
|
||||
"@sentry/node": "^5.25.0",
|
||||
"@sentry/node": "^6.13.2",
|
||||
"@types/fast-levenshtein": "0.0.1",
|
||||
"@types/update-notifier": "^4.1.1",
|
||||
"@zeit/dockerignore": "0.0.3",
|
||||
@ -204,11 +206,11 @@
|
||||
"balena-errors": "^4.7.1",
|
||||
"balena-image-fs": "^7.0.6",
|
||||
"balena-image-manager": "^7.0.3",
|
||||
"balena-preload": "^10.4.20",
|
||||
"balena-release": "^3.0.0",
|
||||
"balena-sdk": "^15.36.0",
|
||||
"balena-preload": "^10.5.0",
|
||||
"balena-release": "^3.2.0",
|
||||
"balena-sdk": "^15.51.1",
|
||||
"balena-semver": "^2.3.0",
|
||||
"balena-settings-client": "^4.0.6",
|
||||
"balena-settings-client": "^4.0.7",
|
||||
"balena-settings-storage": "^7.0.0",
|
||||
"balena-sync": "^11.0.2",
|
||||
"bluebird": "^3.7.2",
|
||||
@ -236,6 +238,7 @@
|
||||
"glob": "^7.1.7",
|
||||
"global-agent": "^2.1.12",
|
||||
"global-tunnel-ng": "^2.1.1",
|
||||
"got": "^11.8.2",
|
||||
"humanize": "0.0.9",
|
||||
"ignore": "^5.1.8",
|
||||
"inquirer": "^7.3.3",
|
||||
@ -246,10 +249,10 @@
|
||||
"livepush": "^3.5.0",
|
||||
"lodash": "^4.17.21",
|
||||
"minimatch": "^3.0.4",
|
||||
"mixpanel": "^0.10.3",
|
||||
"moment": "^2.27.0",
|
||||
"moment-duration-format": "^2.3.2",
|
||||
"ndjson": "^2.0.0",
|
||||
"net-keepalive": "^3.0.0",
|
||||
"node-cleanup": "^2.1.2",
|
||||
"node-unzip-2": "^0.2.8",
|
||||
"oclif": "^1.18.1",
|
||||
@ -262,9 +265,9 @@
|
||||
"request": "^2.88.2",
|
||||
"resin-cli-form": "^2.0.2",
|
||||
"resin-cli-visuals": "^1.8.0",
|
||||
"resin-compose-parse": "^2.1.3",
|
||||
"resin-compose-parse": "^2.3.0",
|
||||
"resin-doodles": "^0.1.1",
|
||||
"resin-multibuild": "^4.11.0",
|
||||
"resin-multibuild": "^4.12.2",
|
||||
"resin-stream-logger": "^0.1.2",
|
||||
"rimraf": "^3.0.2",
|
||||
"semver": "^7.3.2",
|
||||
@ -283,10 +286,9 @@
|
||||
"window-size": "^1.1.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"net-keepalive": "^2.0.3",
|
||||
"windosu": "^0.3.0"
|
||||
},
|
||||
"versionist": {
|
||||
"publishedAt": "2021-08-09T11:15:24.420Z"
|
||||
"publishedAt": "2021-10-05T09:04:41.226Z"
|
||||
}
|
||||
}
|
||||
|
@ -16,7 +16,7 @@
|
||||
*/
|
||||
|
||||
import { expect } from 'chai';
|
||||
import { BalenaAPIMock } from '../../balena-api-mock';
|
||||
import { BalenaAPIMock } from '../../nock/balena-api-mock';
|
||||
import { cleanOutput, runCommand } from '../../helpers';
|
||||
|
||||
const HELP_MESSAGE = '';
|
||||
|
@ -21,10 +21,11 @@ import mock = require('mock-require');
|
||||
import { promises as fs } from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
import { stripIndent } from '../../lib/utils/lazy';
|
||||
import { BalenaAPIMock } from '../balena-api-mock';
|
||||
import { stripIndent } from '../../build/utils/lazy';
|
||||
import { isV13 } from '../../build/utils/version';
|
||||
import { BalenaAPIMock } from '../nock/balena-api-mock';
|
||||
import { expectStreamNoCRLF, testDockerBuildStream } from '../docker-build';
|
||||
import { DockerMock, dockerResponsePath } from '../docker-mock';
|
||||
import { DockerMock, dockerResponsePath } from '../nock/docker-mock';
|
||||
import { cleanOutput, runCommand } from '../helpers';
|
||||
import {
|
||||
ExpectedTarStreamFiles,
|
||||
@ -52,6 +53,16 @@ const commonQueryParams = {
|
||||
labels: '',
|
||||
};
|
||||
|
||||
const commonQueryParamsIntel = {
|
||||
...commonQueryParams,
|
||||
platform: 'linux/amd64',
|
||||
};
|
||||
|
||||
const commonQueryParamsArmV6 = {
|
||||
...commonQueryParams,
|
||||
platform: 'linux/arm/v6',
|
||||
};
|
||||
|
||||
const commonComposeQueryParams = {
|
||||
t: '${tag}',
|
||||
buildargs: {
|
||||
@ -61,8 +72,10 @@ const commonComposeQueryParams = {
|
||||
labels: '',
|
||||
};
|
||||
|
||||
const hr =
|
||||
'----------------------------------------------------------------------';
|
||||
const commonComposeQueryParamsIntel = {
|
||||
...commonComposeQueryParams,
|
||||
platform: 'linux/amd64',
|
||||
};
|
||||
|
||||
// "itSS" means "it() Skip Standalone"
|
||||
const itSS = process.env.BALENA_CLI_TEST_TYPE === 'standalone' ? it.skip : it;
|
||||
@ -78,7 +91,7 @@ describe('balena build', function () {
|
||||
api.expectGetWhoAmI({ optional: true, persist: true });
|
||||
api.expectGetMixpanel({ optional: true });
|
||||
docker.expectGetPing();
|
||||
docker.expectGetVersion();
|
||||
docker.expectGetVersion({ persist: true });
|
||||
});
|
||||
|
||||
this.afterEach(() => {
|
||||
@ -125,11 +138,16 @@ describe('balena build', function () {
|
||||
}
|
||||
}
|
||||
docker.expectGetInfo({});
|
||||
docker.expectGetManifestBusybox();
|
||||
await testDockerBuildStream({
|
||||
commandLine: `build ${projectPath} --deviceType nuc --arch amd64 -g`,
|
||||
commandLine: `build ${projectPath} --deviceType nuc --arch amd64 ${
|
||||
isV13() ? '' : '-g'
|
||||
}`,
|
||||
dockerMock: docker,
|
||||
expectedFilesByService: { main: expectedFiles },
|
||||
expectedQueryParamsByService: { main: Object.entries(commonQueryParams) },
|
||||
expectedQueryParamsByService: {
|
||||
main: Object.entries(commonQueryParamsIntel),
|
||||
},
|
||||
expectedResponseLines,
|
||||
projectPath,
|
||||
responseBody,
|
||||
@ -152,7 +170,7 @@ describe('balena build', function () {
|
||||
'Dockerfile-alt': { fileSize: 30, type: 'file' },
|
||||
};
|
||||
const expectedQueryParams = {
|
||||
...commonQueryParams,
|
||||
...commonQueryParamsIntel,
|
||||
buildargs: '{"BARG1":"b1","barg2":"B2"}',
|
||||
cachefrom: '["my/img1","my/img2"]',
|
||||
};
|
||||
@ -181,6 +199,7 @@ describe('balena build', function () {
|
||||
}
|
||||
}
|
||||
docker.expectGetInfo({});
|
||||
docker.expectGetManifestBusybox();
|
||||
await testDockerBuildStream({
|
||||
commandLine: `build ${projectPath} --deviceType nuc --arch amd64 -B BARG1=b1 -B barg2=B2 --cache-from my/img1,my/img2`,
|
||||
dockerMock: docker,
|
||||
@ -271,12 +290,15 @@ describe('balena build', function () {
|
||||
});
|
||||
mock.reRequire('../../build/utils/qemu');
|
||||
docker.expectGetInfo({ OperatingSystem: 'balenaOS 2.44.0+rev1' });
|
||||
docker.expectGetManifestBusybox();
|
||||
await testDockerBuildStream({
|
||||
commandLine: `build ${projectPath} --emulated --deviceType ${deviceType} --arch ${arch} --nogitignore`,
|
||||
commandLine: `build ${projectPath} --emulated --deviceType ${deviceType} --arch ${arch} ${
|
||||
isV13() ? '' : '--nogitignore'
|
||||
}`,
|
||||
dockerMock: docker,
|
||||
expectedFilesByService: { main: expectedFiles },
|
||||
expectedQueryParamsByService: {
|
||||
main: Object.entries(commonQueryParams),
|
||||
main: Object.entries(commonQueryParamsArmV6),
|
||||
},
|
||||
expectedResponseLines,
|
||||
projectPath,
|
||||
@ -325,11 +347,15 @@ describe('balena build', function () {
|
||||
);
|
||||
}
|
||||
docker.expectGetInfo({});
|
||||
docker.expectGetManifestBusybox();
|
||||
|
||||
await testDockerBuildStream({
|
||||
commandLine: `build ${projectPath} --deviceType nuc --arch amd64 --noconvert-eol -m`,
|
||||
dockerMock: docker,
|
||||
expectedFilesByService: { main: expectedFiles },
|
||||
expectedQueryParamsByService: { main: Object.entries(commonQueryParams) },
|
||||
expectedQueryParamsByService: {
|
||||
main: Object.entries(commonQueryParamsIntel),
|
||||
},
|
||||
expectedResponseLines,
|
||||
projectPath,
|
||||
responseBody,
|
||||
@ -358,7 +384,7 @@ describe('balena build', function () {
|
||||
},
|
||||
service2: {
|
||||
'.dockerignore': { fileSize: 12, type: 'file' },
|
||||
'Dockerfile-alt': { fileSize: 40, type: 'file' },
|
||||
'Dockerfile-alt': { fileSize: 13, type: 'file' },
|
||||
'file2-crlf.sh': {
|
||||
fileSize: isWindows ? 12 : 14,
|
||||
testStream: isWindows ? expectStreamNoCRLF : undefined,
|
||||
@ -384,7 +410,7 @@ describe('balena build', function () {
|
||||
}),
|
||||
),
|
||||
service2: Object.entries(
|
||||
_.merge({}, commonComposeQueryParams, {
|
||||
_.merge({}, commonComposeQueryParamsIntel, {
|
||||
buildargs: {
|
||||
COMPOSE_ARG: 'A',
|
||||
barg: 'b',
|
||||
@ -415,8 +441,12 @@ describe('balena build', function () {
|
||||
);
|
||||
}
|
||||
docker.expectGetInfo({});
|
||||
docker.expectGetManifestNucAlpine();
|
||||
docker.expectGetManifestBusybox();
|
||||
await testDockerBuildStream({
|
||||
commandLine: `build ${projectPath} --deviceType nuc --arch amd64 --convert-eol -G -B COMPOSE_ARG=A -B barg=b --cache-from my/img1,my/img2`,
|
||||
commandLine: `build ${projectPath} --deviceType nuc --arch amd64 --convert-eol ${
|
||||
isV13() ? '' : '-G'
|
||||
} -B COMPOSE_ARG=A -B barg=b --cache-from my/img1,my/img2`,
|
||||
dockerMock: docker,
|
||||
expectedFilesByService,
|
||||
expectedQueryParamsByService,
|
||||
@ -449,7 +479,7 @@ describe('balena build', function () {
|
||||
},
|
||||
service2: {
|
||||
'.dockerignore': { fileSize: 12, type: 'file' },
|
||||
'Dockerfile-alt': { fileSize: 40, type: 'file' },
|
||||
'Dockerfile-alt': { fileSize: 13, type: 'file' },
|
||||
'file2-crlf.sh': {
|
||||
fileSize: isWindows ? 12 : 14,
|
||||
testStream: isWindows ? expectStreamNoCRLF : undefined,
|
||||
@ -469,7 +499,7 @@ describe('balena build', function () {
|
||||
}),
|
||||
),
|
||||
service2: Object.entries(
|
||||
_.merge({}, commonComposeQueryParams, {
|
||||
_.merge({}, commonComposeQueryParamsIntel, {
|
||||
buildargs: {
|
||||
COMPOSE_ARG: 'an argument defined in the docker-compose.yml file',
|
||||
},
|
||||
@ -484,11 +514,11 @@ describe('balena build', function () {
|
||||
'[Build] service2 Step 1/4 : FROM busybox',
|
||||
],
|
||||
...[
|
||||
`[Info] ${hr}`,
|
||||
`[Info] ---------------------------------------------------------------------------`,
|
||||
'[Info] The --multi-dockerignore option is being used, and a .dockerignore file was',
|
||||
'[Info] found at the project source (root) directory. Note that this file will not',
|
||||
'[Info] be used to filter service subdirectories. See "balena help build".',
|
||||
`[Info] ${hr}`,
|
||||
`[Info] ---------------------------------------------------------------------------`,
|
||||
],
|
||||
];
|
||||
if (isWindows) {
|
||||
@ -501,6 +531,9 @@ describe('balena build', function () {
|
||||
);
|
||||
}
|
||||
docker.expectGetInfo({});
|
||||
docker.expectGetManifestBusybox();
|
||||
docker.expectGetManifestNucAlpine();
|
||||
|
||||
await testDockerBuildStream({
|
||||
commandLine: `build ${projectPath} --deviceType nuc --arch amd64 --convert-eol -m`,
|
||||
dockerMock: docker,
|
||||
@ -513,6 +546,99 @@ describe('balena build', function () {
|
||||
services: ['service1', 'service2'],
|
||||
});
|
||||
});
|
||||
|
||||
it('should create the expected tar stream (--projectName and --tag)', async () => {
|
||||
const projectPath = path.join(projectsPath, 'docker-compose', 'basic');
|
||||
const service1Dockerfile = (
|
||||
await fs.readFile(
|
||||
path.join(projectPath, 'service1', 'Dockerfile.template'),
|
||||
'utf8',
|
||||
)
|
||||
).replace('%%BALENA_MACHINE_NAME%%', 'nuc');
|
||||
const expectedFilesByService: ExpectedTarStreamFilesByService = {
|
||||
service1: {
|
||||
Dockerfile: {
|
||||
contents: service1Dockerfile,
|
||||
fileSize: service1Dockerfile.length,
|
||||
type: 'file',
|
||||
},
|
||||
'Dockerfile.template': { fileSize: 144, type: 'file' },
|
||||
'file1.sh': { fileSize: 12, type: 'file' },
|
||||
'test-ignore.txt': { fileSize: 12, type: 'file' },
|
||||
},
|
||||
service2: {
|
||||
'.dockerignore': { fileSize: 12, type: 'file' },
|
||||
'Dockerfile-alt': { fileSize: 13, type: 'file' },
|
||||
'file2-crlf.sh': {
|
||||
fileSize: isWindows ? 12 : 14,
|
||||
testStream: isWindows ? expectStreamNoCRLF : undefined,
|
||||
type: 'file',
|
||||
},
|
||||
},
|
||||
};
|
||||
const responseFilename = 'build-POST.json';
|
||||
const responseBody = await fs.readFile(
|
||||
path.join(dockerResponsePath, responseFilename),
|
||||
'utf8',
|
||||
);
|
||||
const expectedQueryParamsByService = {
|
||||
service1: Object.entries(
|
||||
_.merge({}, commonComposeQueryParams, {
|
||||
buildargs: { SERVICE1_VAR: 'This is a service specific variable' },
|
||||
}),
|
||||
),
|
||||
service2: Object.entries(
|
||||
_.merge({}, commonComposeQueryParamsIntel, {
|
||||
buildargs: {
|
||||
COMPOSE_ARG: 'an argument defined in the docker-compose.yml file',
|
||||
},
|
||||
dockerfile: 'Dockerfile-alt',
|
||||
}),
|
||||
),
|
||||
};
|
||||
const expectedResponseLines: string[] = [
|
||||
...commonResponseLines[responseFilename],
|
||||
...[
|
||||
'[Build] service1 Step 1/4 : FROM busybox',
|
||||
'[Build] service2 Step 1/4 : FROM busybox',
|
||||
],
|
||||
...[
|
||||
`[Info] ---------------------------------------------------------------------------`,
|
||||
'[Info] The --multi-dockerignore option is being used, and a .dockerignore file was',
|
||||
'[Info] found at the project source (root) directory. Note that this file will not',
|
||||
'[Info] be used to filter service subdirectories. See "balena help build".',
|
||||
`[Info] ---------------------------------------------------------------------------`,
|
||||
],
|
||||
];
|
||||
if (isWindows) {
|
||||
expectedResponseLines.push(
|
||||
`[Info] Converting line endings CRLF -> LF for file: ${path.join(
|
||||
projectPath,
|
||||
'service2',
|
||||
'file2-crlf.sh',
|
||||
)}`,
|
||||
);
|
||||
}
|
||||
const projectName = 'spectest';
|
||||
const tag = 'myTag';
|
||||
docker.expectGetInfo({});
|
||||
docker.expectGetManifestBusybox();
|
||||
docker.expectGetManifestNucAlpine();
|
||||
|
||||
await testDockerBuildStream({
|
||||
commandLine: `build ${projectPath} --deviceType nuc --arch amd64 --convert-eol -m --tag ${tag} --projectName ${projectName}`,
|
||||
dockerMock: docker,
|
||||
expectedFilesByService,
|
||||
expectedQueryParamsByService,
|
||||
expectedResponseLines,
|
||||
projectName,
|
||||
projectPath,
|
||||
responseBody,
|
||||
responseCode: 200,
|
||||
services: ['service1', 'service2'],
|
||||
tag,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('balena build: project validation', function () {
|
||||
|
@ -15,15 +15,18 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { Request as ReleaseRequest } from 'balena-release';
|
||||
import { expect } from 'chai';
|
||||
import { promises as fs } from 'fs';
|
||||
import * as _ from 'lodash';
|
||||
import * as nock from 'nock';
|
||||
import * as path from 'path';
|
||||
import * as sinon from 'sinon';
|
||||
|
||||
import { BalenaAPIMock } from '../balena-api-mock';
|
||||
import { isV13 } from '../../build/utils/version';
|
||||
import { BalenaAPIMock } from '../nock/balena-api-mock';
|
||||
import { expectStreamNoCRLF, testDockerBuildStream } from '../docker-build';
|
||||
import { DockerMock, dockerResponsePath } from '../docker-mock';
|
||||
import { DockerMock, dockerResponsePath } from '../nock/docker-mock';
|
||||
import { cleanOutput, runCommand, switchSentry } from '../helpers';
|
||||
import {
|
||||
ExpectedTarStreamFiles,
|
||||
@ -50,6 +53,7 @@ const commonResponseLines = {
|
||||
};
|
||||
|
||||
const commonQueryParams = [
|
||||
['platform', 'linux/arm/v7'],
|
||||
['t', '${tag}'],
|
||||
['buildargs', '{}'],
|
||||
['labels', ''],
|
||||
@ -64,8 +68,10 @@ const commonComposeQueryParams = {
|
||||
labels: '',
|
||||
};
|
||||
|
||||
const hr =
|
||||
'----------------------------------------------------------------------';
|
||||
const commonComposeQueryParamsArmV7 = {
|
||||
...commonComposeQueryParams,
|
||||
platform: 'linux/arm/v7',
|
||||
};
|
||||
|
||||
describe('balena deploy', function () {
|
||||
let api: BalenaAPIMock;
|
||||
@ -77,9 +83,7 @@ describe('balena deploy', function () {
|
||||
docker = new DockerMock();
|
||||
api.expectGetWhoAmI({ optional: true, persist: true });
|
||||
api.expectGetMixpanel({ optional: true });
|
||||
api.expectGetConfigDeviceTypes();
|
||||
api.expectGetApplication();
|
||||
api.expectPostRelease();
|
||||
api.expectGetApplication({ expandArchitecture: true });
|
||||
api.expectGetRelease();
|
||||
api.expectGetUser();
|
||||
api.expectGetService({ serviceName: 'main' });
|
||||
@ -137,12 +141,115 @@ describe('balena deploy', function () {
|
||||
);
|
||||
}
|
||||
|
||||
api.expectPostRelease({});
|
||||
api.expectPatchImage({});
|
||||
api.expectPatchRelease({});
|
||||
api.expectPostImageLabel();
|
||||
docker.expectGetManifestBusybox();
|
||||
|
||||
await testDockerBuildStream({
|
||||
commandLine: `deploy testApp --build --source ${projectPath} -G`,
|
||||
commandLine: `deploy testApp --build --source ${projectPath} ${
|
||||
isV13() ? '' : '-G'
|
||||
}`,
|
||||
dockerMock: docker,
|
||||
expectedFilesByService: { main: expectedFiles },
|
||||
expectedQueryParamsByService: { main: commonQueryParams },
|
||||
expectedResponseLines,
|
||||
projectPath,
|
||||
responseBody,
|
||||
responseCode: 200,
|
||||
services: ['main'],
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle the contract and final status for a final (non-draft) release', async () => {
|
||||
const projectPath = path.join(
|
||||
projectsPath,
|
||||
'no-docker-compose',
|
||||
'with-contract',
|
||||
);
|
||||
const expectedFiles: ExpectedTarStreamFiles = {
|
||||
'src/start.sh': { fileSize: 30, type: 'file' },
|
||||
Dockerfile: { fileSize: 88, type: 'file' },
|
||||
'balena.yml': { fileSize: 55, type: 'file' },
|
||||
};
|
||||
const responseFilename = 'build-POST.json';
|
||||
const responseBody = await fs.readFile(
|
||||
path.join(dockerResponsePath, responseFilename),
|
||||
'utf8',
|
||||
);
|
||||
const expectedResponseLines = [
|
||||
...commonResponseLines[responseFilename],
|
||||
`[Info] No "docker-compose.yml" file found at "${projectPath}"`,
|
||||
`[Info] Creating default composition with source: "${projectPath}"`,
|
||||
];
|
||||
|
||||
api.expectPostRelease({
|
||||
inspectRequest: (_uri: string, requestBody: nock.Body) => {
|
||||
const body = requestBody.valueOf() as Partial<ReleaseRequest>;
|
||||
expect(body.contract).to.be.equal(
|
||||
'{"name":"testContract","type":"sw.application","version":"1.5.2"}',
|
||||
);
|
||||
expect(body.is_final).to.be.true;
|
||||
},
|
||||
});
|
||||
api.expectPatchImage({});
|
||||
api.expectPatchRelease({});
|
||||
api.expectPostImageLabel();
|
||||
docker.expectGetManifestBusybox();
|
||||
|
||||
await testDockerBuildStream({
|
||||
commandLine: `deploy testApp --build --source ${projectPath}`,
|
||||
dockerMock: docker,
|
||||
expectedFilesByService: { main: expectedFiles },
|
||||
expectedQueryParamsByService: { main: commonQueryParams },
|
||||
expectedResponseLines,
|
||||
projectPath,
|
||||
responseBody,
|
||||
responseCode: 200,
|
||||
services: ['main'],
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle the contract and final status for a draft release', async () => {
|
||||
const projectPath = path.join(
|
||||
projectsPath,
|
||||
'no-docker-compose',
|
||||
'with-contract',
|
||||
);
|
||||
const expectedFiles: ExpectedTarStreamFiles = {
|
||||
'src/start.sh': { fileSize: 30, type: 'file' },
|
||||
Dockerfile: { fileSize: 88, type: 'file' },
|
||||
'balena.yml': { fileSize: 55, type: 'file' },
|
||||
};
|
||||
const responseFilename = 'build-POST.json';
|
||||
const responseBody = await fs.readFile(
|
||||
path.join(dockerResponsePath, responseFilename),
|
||||
'utf8',
|
||||
);
|
||||
const expectedResponseLines = [
|
||||
...commonResponseLines[responseFilename],
|
||||
`[Info] No "docker-compose.yml" file found at "${projectPath}"`,
|
||||
`[Info] Creating default composition with source: "${projectPath}"`,
|
||||
];
|
||||
|
||||
api.expectPostRelease({
|
||||
inspectRequest: (_uri: string, requestBody: nock.Body) => {
|
||||
const body = requestBody.valueOf() as Partial<ReleaseRequest>;
|
||||
expect(body.contract).to.be.equal(
|
||||
'{"name":"testContract","type":"sw.application","version":"1.5.2"}',
|
||||
);
|
||||
expect(body.semver).to.be.equal('1.5.2');
|
||||
expect(body.is_final).to.be.false;
|
||||
},
|
||||
});
|
||||
api.expectPatchImage({});
|
||||
api.expectPatchRelease({});
|
||||
api.expectPostImageLabel();
|
||||
docker.expectGetManifestBusybox();
|
||||
|
||||
await testDockerBuildStream({
|
||||
commandLine: `deploy testApp --build --draft --source ${projectPath}`,
|
||||
dockerMock: docker,
|
||||
expectedFilesByService: { main: expectedFiles },
|
||||
expectedQueryParamsByService: { main: commonQueryParams },
|
||||
@ -176,6 +283,9 @@ describe('balena deploy', function () {
|
||||
// causes the CLI to call process.exit() with process.exitCode = 1
|
||||
const expectedExitCode = 1;
|
||||
|
||||
api.expectPostRelease({});
|
||||
docker.expectGetManifestBusybox();
|
||||
|
||||
// Mock this patch HTTP request to return status code 500, in which case
|
||||
// the release status should be saved as "failed" rather than "success"
|
||||
api.expectPatchImage({
|
||||
@ -204,7 +314,9 @@ describe('balena deploy', function () {
|
||||
sinon.stub(process, 'exit');
|
||||
|
||||
await testDockerBuildStream({
|
||||
commandLine: `deploy testApp --build --source ${projectPath} --noconvert-eol -G`,
|
||||
commandLine: `deploy testApp --build --source ${projectPath} --noconvert-eol ${
|
||||
isV13() ? '' : '-G'
|
||||
}`,
|
||||
dockerMock: docker,
|
||||
expectedFilesByService: { main: expectedFiles },
|
||||
expectedQueryParamsByService: { main: commonQueryParams },
|
||||
@ -250,7 +362,7 @@ describe('balena deploy', function () {
|
||||
},
|
||||
service2: {
|
||||
'.dockerignore': { fileSize: 12, type: 'file' },
|
||||
'Dockerfile-alt': { fileSize: 40, type: 'file' },
|
||||
'Dockerfile-alt': { fileSize: 13, type: 'file' },
|
||||
'file2-crlf.sh': {
|
||||
fileSize: isWindows ? 12 : 14,
|
||||
testStream: isWindows ? expectStreamNoCRLF : undefined,
|
||||
@ -270,7 +382,7 @@ describe('balena deploy', function () {
|
||||
}),
|
||||
),
|
||||
service2: Object.entries(
|
||||
_.merge({}, commonComposeQueryParams, {
|
||||
_.merge({}, commonComposeQueryParamsArmV7, {
|
||||
buildargs: {
|
||||
COMPOSE_ARG: 'an argument defined in the docker-compose.yml file',
|
||||
},
|
||||
@ -285,11 +397,11 @@ describe('balena deploy', function () {
|
||||
'[Build] service2 Step 1/4 : FROM busybox',
|
||||
],
|
||||
...[
|
||||
`[Info] ${hr}`,
|
||||
`[Info] ---------------------------------------------------------------------------`,
|
||||
'[Info] The --multi-dockerignore option is being used, and a .dockerignore file was',
|
||||
'[Info] found at the project source (root) directory. Note that this file will not',
|
||||
'[Info] be used to filter service subdirectories. See "balena help deploy".',
|
||||
`[Info] ${hr}`,
|
||||
`[Info] ---------------------------------------------------------------------------`,
|
||||
],
|
||||
];
|
||||
if (isWindows) {
|
||||
@ -302,9 +414,11 @@ describe('balena deploy', function () {
|
||||
);
|
||||
}
|
||||
|
||||
// docker.expectGetImages();
|
||||
api.expectPostRelease({});
|
||||
api.expectPatchImage({});
|
||||
api.expectPatchRelease({});
|
||||
docker.expectGetManifestRpi3Alpine();
|
||||
docker.expectGetManifestBusybox();
|
||||
|
||||
await testDockerBuildStream({
|
||||
commandLine: `deploy testApp --build --source ${projectPath} --multi-dockerignore`,
|
||||
|
@ -16,7 +16,7 @@
|
||||
*/
|
||||
|
||||
import { expect } from 'chai';
|
||||
import { BalenaAPIMock } from '../../balena-api-mock';
|
||||
import { BalenaAPIMock } from '../../nock/balena-api-mock';
|
||||
import { cleanOutput, runCommand } from '../../helpers';
|
||||
|
||||
describe('balena device move', function () {
|
||||
|
@ -18,7 +18,7 @@
|
||||
import { expect } from 'chai';
|
||||
import * as path from 'path';
|
||||
|
||||
import { apiResponsePath, BalenaAPIMock } from '../../balena-api-mock';
|
||||
import { apiResponsePath, BalenaAPIMock } from '../../nock/balena-api-mock';
|
||||
import { cleanOutput, runCommand } from '../../helpers';
|
||||
|
||||
import { appToFleetOutputMsg, warnify } from '../../../build/utils/messages';
|
||||
|
@ -18,7 +18,7 @@
|
||||
import { expect } from 'chai';
|
||||
import * as path from 'path';
|
||||
|
||||
import { apiResponsePath, BalenaAPIMock } from '../../balena-api-mock';
|
||||
import { apiResponsePath, BalenaAPIMock } from '../../nock/balena-api-mock';
|
||||
import { cleanOutput, runCommand } from '../../helpers';
|
||||
|
||||
import { appToFleetOutputMsg, warnify } from '../../../build/utils/messages';
|
||||
@ -59,8 +59,9 @@ describe('balena devices', function () {
|
||||
const lines = cleanOutput(out);
|
||||
|
||||
expect(lines[0].replace(/ +/g, ' ')).to.equal(
|
||||
'ID UUID DEVICE NAME DEVICE TYPE APPLICATION NAME STATUS ' +
|
||||
'IS ONLINE SUPERVISOR VERSION OS VERSION DASHBOARD URL',
|
||||
isV13()
|
||||
? 'ID UUID DEVICE NAME DEVICE TYPE FLEET STATUS IS ONLINE SUPERVISOR VERSION OS VERSION DASHBOARD URL'
|
||||
: 'ID UUID DEVICE NAME DEVICE TYPE APPLICATION NAME STATUS IS ONLINE SUPERVISOR VERSION OS VERSION DASHBOARD URL',
|
||||
);
|
||||
expect(lines).to.have.lengthOf.at.least(2);
|
||||
|
||||
|
@ -17,10 +17,10 @@
|
||||
|
||||
import { expect } from 'chai';
|
||||
|
||||
import { BalenaAPIMock } from '../../balena-api-mock';
|
||||
import { BalenaAPIMock } from '../../nock/balena-api-mock';
|
||||
import { cleanOutput, runCommand } from '../../helpers';
|
||||
|
||||
import { isV13 } from '../../../lib/utils/version';
|
||||
import { isV13 } from '../../../build/utils/version';
|
||||
|
||||
describe('balena devices supported', function () {
|
||||
let api: BalenaAPIMock;
|
||||
|
2
tests/commands/env/add.spec.ts
vendored
2
tests/commands/env/add.spec.ts
vendored
@ -17,7 +17,7 @@
|
||||
|
||||
import { expect } from 'chai';
|
||||
|
||||
import { BalenaAPIMock } from '../../balena-api-mock';
|
||||
import { BalenaAPIMock } from '../../nock/balena-api-mock';
|
||||
import { runCommand } from '../../helpers';
|
||||
|
||||
describe('balena env add', function () {
|
||||
|
245
tests/commands/env/envs.spec.ts
vendored
245
tests/commands/env/envs.spec.ts
vendored
@ -16,16 +16,12 @@
|
||||
*/
|
||||
|
||||
import { expect } from 'chai';
|
||||
import { stripIndent } from '../../../lib/utils/lazy';
|
||||
import { stripIndent } from '../../../build/utils/lazy';
|
||||
|
||||
import { BalenaAPIMock } from '../../balena-api-mock';
|
||||
import { BalenaAPIMock } from '../../nock/balena-api-mock';
|
||||
import { runCommand } from '../../helpers';
|
||||
|
||||
import {
|
||||
appToFleetFlagMsg,
|
||||
appToFleetOutputMsg,
|
||||
warnify,
|
||||
} from '../../../build/utils/messages';
|
||||
import { appToFleetOutputMsg, warnify } from '../../../build/utils/messages';
|
||||
import { isV13 } from '../../../build/utils/version';
|
||||
|
||||
describe('balena envs', function () {
|
||||
@ -48,13 +44,6 @@ describe('balena envs', function () {
|
||||
api.done();
|
||||
});
|
||||
|
||||
const appToFleetFlagWarn =
|
||||
!isV13() &&
|
||||
process.stderr.isTTY &&
|
||||
process.env.BALENA_CLI_TEST_TYPE !== 'standalone'
|
||||
? warnify(appToFleetFlagMsg) + '\n'
|
||||
: '';
|
||||
|
||||
const appToFleetOutputWarn =
|
||||
!isV13() &&
|
||||
process.stderr.isTTY &&
|
||||
@ -67,18 +56,18 @@ describe('balena envs', function () {
|
||||
api.expectGetAppEnvVars();
|
||||
api.expectGetAppServiceVars();
|
||||
|
||||
const { out, err } = await runCommand(`envs -a ${appName}`);
|
||||
const { out, err } = await runCommand(`envs -f ${appName}`);
|
||||
|
||||
expect(out.join('')).to.equal(
|
||||
stripIndent`
|
||||
ID NAME VALUE APPLICATION SERVICE
|
||||
120110 svar1 svar1-value test service1
|
||||
120111 svar2 svar2-value test service2
|
||||
120101 var1 var1-val test *
|
||||
120102 var2 22 test *
|
||||
ID NAME VALUE FLEET SERVICE
|
||||
120110 svar1 svar1-value test service1
|
||||
120111 svar2 svar2-value test service2
|
||||
120101 var1 var1-val test *
|
||||
120102 var2 22 test *
|
||||
` + '\n',
|
||||
);
|
||||
expect(err.join('')).to.equal(appToFleetFlagWarn);
|
||||
expect(err.join('')).to.equal('');
|
||||
});
|
||||
|
||||
it('should successfully list config vars for a test fleet', async () => {
|
||||
@ -101,11 +90,11 @@ describe('balena envs', function () {
|
||||
api.expectGetApplication();
|
||||
api.expectGetAppConfigVars();
|
||||
|
||||
const { out, err } = await runCommand(`envs -cja ${appName}`);
|
||||
const { out, err } = await runCommand(`envs -cjf ${appName}`);
|
||||
|
||||
expect(JSON.parse(out.join(''))).to.deep.equal([
|
||||
{
|
||||
appName: 'test',
|
||||
fleetName: 'test',
|
||||
id: 120300,
|
||||
name: 'RESIN_SUPERVISOR_NATIVE_LOGGER',
|
||||
value: 'false',
|
||||
@ -122,18 +111,18 @@ describe('balena envs', function () {
|
||||
api.expectGetAppServiceVars();
|
||||
|
||||
const { out, err } = await runCommand(
|
||||
`envs -a ${appName} -s ${serviceName}`,
|
||||
`envs -f ${appName} -s ${serviceName}`,
|
||||
);
|
||||
|
||||
expect(out.join('')).to.equal(
|
||||
stripIndent`
|
||||
ID NAME VALUE APPLICATION SERVICE
|
||||
120111 svar2 svar2-value test service2
|
||||
120101 var1 var1-val test *
|
||||
120102 var2 22 test *
|
||||
ID NAME VALUE FLEET SERVICE
|
||||
120111 svar2 svar2-value test service2
|
||||
120101 var1 var1-val test *
|
||||
120102 var2 22 test *
|
||||
` + '\n',
|
||||
);
|
||||
expect(err.join('')).to.equal(appToFleetFlagWarn);
|
||||
expect(err.join('')).to.equal('');
|
||||
});
|
||||
|
||||
it('should successfully list env and service vars for a test fleet (-s flags)', async () => {
|
||||
@ -144,18 +133,18 @@ describe('balena envs', function () {
|
||||
api.expectGetAppServiceVars();
|
||||
|
||||
const { out, err } = await runCommand(
|
||||
`envs -a ${appName} -s ${serviceName}`,
|
||||
`envs -f ${appName} -s ${serviceName}`,
|
||||
);
|
||||
|
||||
expect(out.join('')).to.equal(
|
||||
stripIndent`
|
||||
ID NAME VALUE APPLICATION SERVICE
|
||||
120110 svar1 svar1-value test ${serviceName}
|
||||
120101 var1 var1-val test *
|
||||
120102 var2 22 test *
|
||||
ID NAME VALUE FLEET SERVICE
|
||||
120110 svar1 svar1-value test ${serviceName}
|
||||
120101 var1 var1-val test *
|
||||
120102 var2 22 test *
|
||||
` + '\n',
|
||||
);
|
||||
expect(err.join('')).to.equal(appToFleetFlagWarn);
|
||||
expect(err.join('')).to.equal('');
|
||||
});
|
||||
|
||||
it('should successfully list env variables for a test device', async () => {
|
||||
@ -167,22 +156,29 @@ describe('balena envs', function () {
|
||||
api.expectGetDeviceServiceVars();
|
||||
|
||||
const uuid = shortUUID;
|
||||
const { out, err } = await runCommand(`envs -d ${uuid}`);
|
||||
|
||||
expect(out.join('')).to.equal(
|
||||
const result = await runCommand(`envs -d ${uuid}`);
|
||||
const { err } = result;
|
||||
let { out } = result;
|
||||
let expected =
|
||||
stripIndent`
|
||||
ID NAME VALUE APPLICATION DEVICE SERVICE
|
||||
120110 svar1 svar1-value test * service1
|
||||
120111 svar2 svar2-value test * service2
|
||||
120120 svar3 svar3-value test ${uuid} service1
|
||||
120121 svar4 svar4-value test ${uuid} service2
|
||||
120101 var1 var1-val test * *
|
||||
120102 var2 22 test * *
|
||||
120203 var3 var3-val test ${uuid} *
|
||||
120204 var4 44 test ${uuid} *
|
||||
` + '\n',
|
||||
);
|
||||
|
||||
ID NAME VALUE APPLICATION DEVICE SERVICE
|
||||
120110 svar1 svar1-value test * service1
|
||||
120111 svar2 svar2-value test * service2
|
||||
120120 svar3 svar3-value test ${uuid} service1
|
||||
120121 svar4 svar4-value test ${uuid} service2
|
||||
120101 var1 var1-val test * *
|
||||
120102 var2 22 test * *
|
||||
120203 var3 var3-val test ${uuid} *
|
||||
120204 var4 44 test ${uuid} *
|
||||
` + '\n';
|
||||
if (isV13()) {
|
||||
out = out.map((l) => l.replace(/ +/g, ' '));
|
||||
expected = expected
|
||||
.replace(/ +/g, ' ')
|
||||
.replace(' APPLICATION ', ' FLEET ')
|
||||
.replace(/ test /g, ' org/test ');
|
||||
}
|
||||
expect(out.join('')).to.equal(expected);
|
||||
expect(err.join('')).to.equal(appToFleetOutputWarn);
|
||||
});
|
||||
|
||||
@ -195,9 +191,7 @@ describe('balena envs', function () {
|
||||
api.expectGetDeviceServiceVars();
|
||||
|
||||
const { out, err } = await runCommand(`envs -jd ${shortUUID}`);
|
||||
|
||||
expect(JSON.parse(out.join(''))).to.deep.equal(
|
||||
JSON.parse(`[
|
||||
let expected = `[
|
||||
{ "id": 120101, "appName": "test", "deviceUUID": "*", "name": "var1", "value": "var1-val", "serviceName": "*" },
|
||||
{ "id": 120102, "appName": "test", "deviceUUID": "*", "name": "var2", "value": "22", "serviceName": "*" },
|
||||
{ "id": 120110, "appName": "test", "deviceUUID": "*", "name": "svar1", "value": "svar1-value", "serviceName": "service1" },
|
||||
@ -206,9 +200,14 @@ describe('balena envs', function () {
|
||||
{ "id": 120121, "appName": "test", "deviceUUID": "${fullUUID}", "name": "svar4", "value": "svar4-value", "serviceName": "service2" },
|
||||
{ "id": 120203, "appName": "test", "deviceUUID": "${fullUUID}", "name": "var3", "value": "var3-val", "serviceName": "*" },
|
||||
{ "id": 120204, "appName": "test", "deviceUUID": "${fullUUID}", "name": "var4", "value": "44", "serviceName": "*" }
|
||||
]`),
|
||||
);
|
||||
|
||||
]`;
|
||||
if (isV13()) {
|
||||
expected = expected.replace(
|
||||
/"appName": "test"/g,
|
||||
'"fleetName": "org/test"',
|
||||
);
|
||||
}
|
||||
expect(JSON.parse(out.join(''))).to.deep.equal(JSON.parse(expected));
|
||||
expect(err.join('')).to.equal('');
|
||||
});
|
||||
|
||||
@ -218,16 +217,23 @@ describe('balena envs', function () {
|
||||
api.expectGetApplication();
|
||||
api.expectGetAppConfigVars();
|
||||
|
||||
const { out, err } = await runCommand(`envs -d ${shortUUID} --config`);
|
||||
|
||||
expect(out.join('')).to.equal(
|
||||
const result = await runCommand(`envs -d ${shortUUID} --config`);
|
||||
const { err } = result;
|
||||
let { out } = result;
|
||||
let expected =
|
||||
stripIndent`
|
||||
ID NAME VALUE APPLICATION DEVICE
|
||||
120300 RESIN_SUPERVISOR_NATIVE_LOGGER false test *
|
||||
120400 RESIN_SUPERVISOR_POLL_INTERVAL 900900 test ${shortUUID}
|
||||
` + '\n',
|
||||
);
|
||||
|
||||
ID NAME VALUE APPLICATION DEVICE
|
||||
120300 RESIN_SUPERVISOR_NATIVE_LOGGER false test *
|
||||
120400 RESIN_SUPERVISOR_POLL_INTERVAL 900900 test ${shortUUID}
|
||||
` + '\n';
|
||||
if (isV13()) {
|
||||
out = out.map((l) => l.replace(/ +/g, ' '));
|
||||
expected = expected
|
||||
.replace(/ +/g, ' ')
|
||||
.replace(' APPLICATION ', ' FLEET ')
|
||||
.replace(/ test /g, ' org/test ');
|
||||
}
|
||||
expect(out.join('')).to.equal(expected);
|
||||
expect(err.join('')).to.equal(appToFleetOutputWarn);
|
||||
});
|
||||
|
||||
@ -242,20 +248,27 @@ describe('balena envs', function () {
|
||||
api.expectGetDeviceEnvVars();
|
||||
|
||||
const uuid = shortUUID;
|
||||
const { out, err } = await runCommand(`envs -d ${uuid} -s ${serviceName}`);
|
||||
|
||||
expect(out.join('')).to.equal(
|
||||
const result = await runCommand(`envs -d ${uuid} -s ${serviceName}`);
|
||||
const { err } = result;
|
||||
let { out } = result;
|
||||
let expected =
|
||||
stripIndent`
|
||||
ID NAME VALUE APPLICATION DEVICE SERVICE
|
||||
120111 svar2 svar2-value test * service2
|
||||
120121 svar4 svar4-value test ${uuid} service2
|
||||
120101 var1 var1-val test * *
|
||||
120102 var2 22 test * *
|
||||
120203 var3 var3-val test ${uuid} *
|
||||
120204 var4 44 test ${uuid} *
|
||||
` + '\n',
|
||||
);
|
||||
|
||||
ID NAME VALUE APPLICATION DEVICE SERVICE
|
||||
120111 svar2 svar2-value test * service2
|
||||
120121 svar4 svar4-value test ${uuid} service2
|
||||
120101 var1 var1-val test * *
|
||||
120102 var2 22 test * *
|
||||
120203 var3 var3-val test ${uuid} *
|
||||
120204 var4 44 test ${uuid} *
|
||||
` + '\n';
|
||||
if (isV13()) {
|
||||
out = out.map((l) => l.replace(/ +/g, ' '));
|
||||
expected = expected
|
||||
.replace(/ +/g, ' ')
|
||||
.replace(' APPLICATION ', ' FLEET ')
|
||||
.replace(/ test /g, ' org/test ');
|
||||
}
|
||||
expect(out.join('')).to.equal(expected);
|
||||
expect(err.join('')).to.equal(appToFleetOutputWarn);
|
||||
});
|
||||
|
||||
@ -265,18 +278,24 @@ describe('balena envs', function () {
|
||||
api.expectGetDeviceServiceVars();
|
||||
|
||||
const uuid = shortUUID;
|
||||
|
||||
const { out, err } = await runCommand(`envs -d ${uuid}`);
|
||||
|
||||
expect(out.join('')).to.equal(
|
||||
const result = await runCommand(`envs -d ${uuid}`);
|
||||
const { err } = result;
|
||||
let { out } = result;
|
||||
let expected =
|
||||
stripIndent`
|
||||
ID NAME VALUE APPLICATION DEVICE SERVICE
|
||||
120120 svar3 svar3-value N/A ${uuid} service1
|
||||
120121 svar4 svar4-value N/A ${uuid} service2
|
||||
120203 var3 var3-val N/A ${uuid} *
|
||||
120204 var4 44 N/A ${uuid} *
|
||||
` + '\n',
|
||||
);
|
||||
` + '\n';
|
||||
if (isV13()) {
|
||||
out = out.map((l) => l.replace(/ +/g, ' '));
|
||||
expected = expected
|
||||
.replace(/ +/g, ' ')
|
||||
.replace(' APPLICATION ', ' FLEET ');
|
||||
}
|
||||
expect(out.join('')).to.equal(expected);
|
||||
expect(err.join('')).to.equal(appToFleetOutputWarn);
|
||||
});
|
||||
|
||||
@ -291,19 +310,27 @@ describe('balena envs', function () {
|
||||
api.expectGetDeviceServiceVars();
|
||||
|
||||
const uuid = shortUUID;
|
||||
const { out, err } = await runCommand(`envs -d ${uuid} -s ${serviceName}`);
|
||||
|
||||
expect(out.join('')).to.equal(
|
||||
const result = await runCommand(`envs -d ${uuid} -s ${serviceName}`);
|
||||
const { err } = result;
|
||||
let { out } = result;
|
||||
let expected =
|
||||
stripIndent`
|
||||
ID NAME VALUE APPLICATION DEVICE SERVICE
|
||||
120110 svar1 svar1-value test * ${serviceName}
|
||||
120120 svar3 svar3-value test ${uuid} ${serviceName}
|
||||
120101 var1 var1-val test * *
|
||||
120102 var2 22 test * *
|
||||
120203 var3 var3-val test ${uuid} *
|
||||
120204 var4 44 test ${uuid} *
|
||||
` + '\n',
|
||||
);
|
||||
ID NAME VALUE APPLICATION DEVICE SERVICE
|
||||
120110 svar1 svar1-value test * ${serviceName}
|
||||
120120 svar3 svar3-value test ${uuid} ${serviceName}
|
||||
120101 var1 var1-val test * *
|
||||
120102 var2 22 test * *
|
||||
120203 var3 var3-val test ${uuid} *
|
||||
120204 var4 44 test ${uuid} *
|
||||
` + '\n';
|
||||
if (isV13()) {
|
||||
out = out.map((l) => l.replace(/ +/g, ' '));
|
||||
expected = expected
|
||||
.replace(/ +/g, ' ')
|
||||
.replace(' APPLICATION ', ' FLEET ')
|
||||
.replace(/ test /g, ' org/test ');
|
||||
}
|
||||
expect(out.join('')).to.equal(expected);
|
||||
expect(err.join('')).to.equal(appToFleetOutputWarn);
|
||||
});
|
||||
|
||||
@ -320,17 +347,21 @@ describe('balena envs', function () {
|
||||
const { out, err } = await runCommand(
|
||||
`envs -d ${shortUUID} -js ${serviceName}`,
|
||||
);
|
||||
|
||||
expect(JSON.parse(out.join(''))).to.deep.equal(
|
||||
JSON.parse(`[
|
||||
{ "id": 120101, "appName": "test", "deviceUUID": "*", "name": "var1", "value": "var1-val", "serviceName": "*" },
|
||||
{ "id": 120102, "appName": "test", "deviceUUID": "*", "name": "var2", "value": "22", "serviceName": "*" },
|
||||
{ "id": 120110, "appName": "test", "deviceUUID": "*", "name": "svar1", "value": "svar1-value", "serviceName": "${serviceName}" },
|
||||
{ "id": 120120, "appName": "test", "deviceUUID": "${fullUUID}", "name": "svar3", "value": "svar3-value", "serviceName": "${serviceName}" },
|
||||
{ "id": 120203, "appName": "test", "deviceUUID": "${fullUUID}", "name": "var3", "value": "var3-val", "serviceName": "*" },
|
||||
{ "id": 120204, "appName": "test", "deviceUUID": "${fullUUID}", "name": "var4", "value": "44", "serviceName": "*" }
|
||||
]`),
|
||||
);
|
||||
let expected = `[
|
||||
{ "id": 120101, "appName": "test", "deviceUUID": "*", "name": "var1", "value": "var1-val", "serviceName": "*" },
|
||||
{ "id": 120102, "appName": "test", "deviceUUID": "*", "name": "var2", "value": "22", "serviceName": "*" },
|
||||
{ "id": 120110, "appName": "test", "deviceUUID": "*", "name": "svar1", "value": "svar1-value", "serviceName": "${serviceName}" },
|
||||
{ "id": 120120, "appName": "test", "deviceUUID": "${fullUUID}", "name": "svar3", "value": "svar3-value", "serviceName": "${serviceName}" },
|
||||
{ "id": 120203, "appName": "test", "deviceUUID": "${fullUUID}", "name": "var3", "value": "var3-val", "serviceName": "*" },
|
||||
{ "id": 120204, "appName": "test", "deviceUUID": "${fullUUID}", "name": "var4", "value": "44", "serviceName": "*" }
|
||||
]`;
|
||||
if (isV13()) {
|
||||
expected = expected.replace(
|
||||
/"appName": "test"/g,
|
||||
'"fleetName": "org/test"',
|
||||
);
|
||||
}
|
||||
expect(JSON.parse(out.join(''))).to.deep.equal(JSON.parse(expected));
|
||||
expect(err.join('')).to.equal('');
|
||||
});
|
||||
});
|
||||
|
2
tests/commands/env/rename.spec.ts
vendored
2
tests/commands/env/rename.spec.ts
vendored
@ -17,7 +17,7 @@
|
||||
|
||||
import { expect } from 'chai';
|
||||
|
||||
import { BalenaAPIMock } from '../../balena-api-mock';
|
||||
import { BalenaAPIMock } from '../../nock/balena-api-mock';
|
||||
import { runCommand } from '../../helpers';
|
||||
|
||||
describe('balena env rename', function () {
|
||||
|
2
tests/commands/env/rm.spec.ts
vendored
2
tests/commands/env/rm.spec.ts
vendored
@ -17,7 +17,7 @@
|
||||
|
||||
import { expect } from 'chai';
|
||||
|
||||
import { BalenaAPIMock } from '../../balena-api-mock';
|
||||
import { BalenaAPIMock } from '../../nock/balena-api-mock';
|
||||
import { runCommand } from '../../helpers';
|
||||
|
||||
describe('balena env rm', function () {
|
||||
|
@ -17,7 +17,7 @@
|
||||
|
||||
import { expect } from 'chai';
|
||||
|
||||
import { BalenaAPIMock } from '../balena-api-mock';
|
||||
import { BalenaAPIMock } from '../nock/balena-api-mock';
|
||||
import { cleanOutput, runCommand } from '../helpers';
|
||||
import * as messages from '../../build/utils/messages';
|
||||
|
||||
|
@ -17,9 +17,9 @@
|
||||
|
||||
import { expect } from 'chai';
|
||||
|
||||
import { BalenaAPIMock } from '../balena-api-mock';
|
||||
import { BalenaAPIMock } from '../nock/balena-api-mock';
|
||||
import { cleanOutput, runCommand } from '../helpers';
|
||||
import { SupervisorMock } from '../supervisor-mock';
|
||||
import { SupervisorMock } from '../nock/supervisor-mock';
|
||||
|
||||
const itS = process.env.BALENA_CLI_TEST_TYPE === 'standalone' ? it : it.skip;
|
||||
|
||||
|
@ -25,7 +25,7 @@ import * as tmp from 'tmp';
|
||||
tmp.setGracefulCleanup();
|
||||
const tmpNameAsync = promisify(tmp.tmpName);
|
||||
|
||||
import { BalenaAPIMock } from '../../balena-api-mock';
|
||||
import { BalenaAPIMock } from '../../nock/balena-api-mock';
|
||||
|
||||
if (process.platform !== 'win32') {
|
||||
describe('balena os configure', function () {
|
||||
|
@ -19,8 +19,9 @@ import { expect } from 'chai';
|
||||
import { promises as fs } from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
import { BalenaAPIMock } from '../balena-api-mock';
|
||||
import { BuilderMock, builderResponsePath } from '../builder-mock';
|
||||
import { isV13 } from '../../build/utils/version';
|
||||
import { BalenaAPIMock } from '../nock/balena-api-mock';
|
||||
import { BuilderMock, builderResponsePath } from '../nock/builder-mock';
|
||||
import { expectStreamNoCRLF, testPushBuildStream } from '../docker-build';
|
||||
import { cleanOutput, runCommand } from '../helpers';
|
||||
import {
|
||||
@ -34,6 +35,8 @@ import {
|
||||
const repoPath = path.normalize(path.join(__dirname, '..', '..'));
|
||||
const projectsPath = path.join(repoPath, 'tests', 'test-data', 'projects');
|
||||
|
||||
const itNoV13 = isV13() ? it.skip : it;
|
||||
|
||||
const commonResponseLines = {
|
||||
'build-POST-v3.json': [
|
||||
'[Info] Starting build for testApp, user gh_user',
|
||||
@ -73,6 +76,7 @@ const commonQueryParams = [
|
||||
['emulated', 'false'],
|
||||
['nocache', 'false'],
|
||||
['headless', 'false'],
|
||||
['isdraft', 'false'],
|
||||
];
|
||||
|
||||
const hr =
|
||||
@ -233,71 +237,74 @@ describe('balena push', function () {
|
||||
});
|
||||
});
|
||||
|
||||
it('should create the expected tar stream (single container, --gitignore)', async () => {
|
||||
const projectPath = path.join(
|
||||
projectsPath,
|
||||
'no-docker-compose',
|
||||
'dockerignore1',
|
||||
);
|
||||
const expectedFiles: ExpectedTarStreamFiles = {
|
||||
'.balena/balena.yml': { fileSize: 12, type: 'file' },
|
||||
'.dockerignore': { fileSize: 438, type: 'file' },
|
||||
'.gitignore': { fileSize: 20, type: 'file' },
|
||||
'.git/bar.txt': { fileSize: 4, type: 'file' },
|
||||
'.git/foo.txt': { fileSize: 4, type: 'file' },
|
||||
'c.txt': { fileSize: 1, type: 'file' },
|
||||
Dockerfile: { fileSize: 13, type: 'file' },
|
||||
'src/.balena/balena.yml': { fileSize: 16, type: 'file' },
|
||||
'src/.gitignore': { fileSize: 10, type: 'file' },
|
||||
'vendor/.git/vendor-git-contents': { fileSize: 20, type: 'file' },
|
||||
// When --gitignore (-g) is provided for v11 compatibility, the old
|
||||
// `zeit/dockerignore` npm package is still used but it is broken on
|
||||
// Windows (reason why we created `@balena/dockerignore`).
|
||||
...(isWindows
|
||||
? {
|
||||
'src/src-b.txt': { fileSize: 5, type: 'file' },
|
||||
'dot.git/bar.txt': { fileSize: 4, type: 'file' },
|
||||
'dot.git/foo.txt': { fileSize: 4, type: 'file' },
|
||||
'vendor/dot.git/vendor-git-contents': {
|
||||
fileSize: 20,
|
||||
type: 'file',
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
};
|
||||
itNoV13(
|
||||
'should create the expected tar stream (single container, --gitignore)',
|
||||
async () => {
|
||||
const projectPath = path.join(
|
||||
projectsPath,
|
||||
'no-docker-compose',
|
||||
'dockerignore1',
|
||||
);
|
||||
const expectedFiles: ExpectedTarStreamFiles = {
|
||||
'.balena/balena.yml': { fileSize: 12, type: 'file' },
|
||||
'.dockerignore': { fileSize: 438, type: 'file' },
|
||||
'.gitignore': { fileSize: 20, type: 'file' },
|
||||
'.git/bar.txt': { fileSize: 4, type: 'file' },
|
||||
'.git/foo.txt': { fileSize: 4, type: 'file' },
|
||||
'c.txt': { fileSize: 1, type: 'file' },
|
||||
Dockerfile: { fileSize: 13, type: 'file' },
|
||||
'src/.balena/balena.yml': { fileSize: 16, type: 'file' },
|
||||
'src/.gitignore': { fileSize: 10, type: 'file' },
|
||||
'vendor/.git/vendor-git-contents': { fileSize: 20, type: 'file' },
|
||||
// When --gitignore (-g) is provided for v11 compatibility, the old
|
||||
// `zeit/dockerignore` npm package is still used but it is broken on
|
||||
// Windows (reason why we created `@balena/dockerignore`).
|
||||
...(isWindows
|
||||
? {
|
||||
'src/src-b.txt': { fileSize: 5, type: 'file' },
|
||||
'dot.git/bar.txt': { fileSize: 4, type: 'file' },
|
||||
'dot.git/foo.txt': { fileSize: 4, type: 'file' },
|
||||
'vendor/dot.git/vendor-git-contents': {
|
||||
fileSize: 20,
|
||||
type: 'file',
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
};
|
||||
|
||||
const regSecretsPath = await addRegSecretsEntries(expectedFiles);
|
||||
const responseFilename = 'build-POST-v3.json';
|
||||
const responseBody = await fs.readFile(
|
||||
path.join(builderResponsePath, responseFilename),
|
||||
'utf8',
|
||||
);
|
||||
const expectedResponseLines = [
|
||||
...[
|
||||
`[Warn] ${hr}`,
|
||||
'[Warn] Using file ignore patterns from:',
|
||||
`[Warn] * ${path.join(projectPath, '.dockerignore')}`,
|
||||
`[Warn] * ${path.join(projectPath, '.gitignore')}`,
|
||||
`[Warn] * ${path.join(projectPath, 'src', '.gitignore')}`,
|
||||
'[Warn] .gitignore files are being considered because the --gitignore option was used.',
|
||||
'[Warn] This option is deprecated and will be removed in the next major version release.',
|
||||
"[Warn] For more information, see 'balena help push'.",
|
||||
`[Warn] ${hr}`,
|
||||
],
|
||||
...commonResponseLines[responseFilename],
|
||||
];
|
||||
const regSecretsPath = await addRegSecretsEntries(expectedFiles);
|
||||
const responseFilename = 'build-POST-v3.json';
|
||||
const responseBody = await fs.readFile(
|
||||
path.join(builderResponsePath, responseFilename),
|
||||
'utf8',
|
||||
);
|
||||
const expectedResponseLines = [
|
||||
...[
|
||||
`[Warn] ${hr}`,
|
||||
'[Warn] Using file ignore patterns from:',
|
||||
`[Warn] * ${path.join(projectPath, '.dockerignore')}`,
|
||||
`[Warn] * ${path.join(projectPath, '.gitignore')}`,
|
||||
`[Warn] * ${path.join(projectPath, 'src', '.gitignore')}`,
|
||||
'[Warn] .gitignore files are being considered because the --gitignore option was used.',
|
||||
'[Warn] This option is deprecated and will be removed in the next major version release.',
|
||||
"[Warn] For more information, see 'balena help push'.",
|
||||
`[Warn] ${hr}`,
|
||||
],
|
||||
...commonResponseLines[responseFilename],
|
||||
];
|
||||
|
||||
await testPushBuildStream({
|
||||
builderMock: builder,
|
||||
commandLine: `push testApp -s ${projectPath} -R ${regSecretsPath} -g`,
|
||||
expectedFiles,
|
||||
expectedQueryParams: commonQueryParams,
|
||||
expectedResponseLines,
|
||||
projectPath,
|
||||
responseBody,
|
||||
responseCode: 200,
|
||||
});
|
||||
});
|
||||
await testPushBuildStream({
|
||||
builderMock: builder,
|
||||
commandLine: `push testApp -s ${projectPath} -R ${regSecretsPath} -g`,
|
||||
expectedFiles,
|
||||
expectedQueryParams: commonQueryParams,
|
||||
expectedResponseLines,
|
||||
projectPath,
|
||||
responseBody,
|
||||
responseCode: 200,
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
it('should create the expected tar stream (single container, --nogitignore)', async () => {
|
||||
const projectPath = path.join(
|
||||
@ -369,24 +376,27 @@ describe('balena push', function () {
|
||||
path.join(builderResponsePath, responseFilename),
|
||||
'utf8',
|
||||
);
|
||||
const expectedResponseLines = isWindows
|
||||
? [
|
||||
`[Warn] ${hr}`,
|
||||
'[Warn] Using file ignore patterns from:',
|
||||
`[Warn] * ${path.join(projectPath, '.dockerignore')}`,
|
||||
'[Warn] The --gitignore option was used, but no .gitignore files were found.',
|
||||
'[Warn] The --gitignore option is deprecated and will be removed in the next major',
|
||||
'[Warn] version release. It prevents the use of a better dockerignore parser and',
|
||||
'[Warn] filter library that fixes several issues on Windows and improves compatibility',
|
||||
"[Warn] with 'docker build'. For more information, see 'balena help push'.",
|
||||
`[Warn] ${hr}`,
|
||||
...commonResponseLines[responseFilename],
|
||||
]
|
||||
: commonResponseLines[responseFilename];
|
||||
const expectedResponseLines =
|
||||
!isV13() && isWindows
|
||||
? [
|
||||
`[Warn] ${hr}`,
|
||||
'[Warn] Using file ignore patterns from:',
|
||||
`[Warn] * ${path.join(projectPath, '.dockerignore')}`,
|
||||
'[Warn] The --gitignore option was used, but no .gitignore files were found.',
|
||||
'[Warn] The --gitignore option is deprecated and will be removed in the next major',
|
||||
'[Warn] version release. It prevents the use of a better dockerignore parser and',
|
||||
'[Warn] filter library that fixes several issues on Windows and improves compatibility',
|
||||
"[Warn] with 'docker build'. For more information, see 'balena help push'.",
|
||||
`[Warn] ${hr}`,
|
||||
...commonResponseLines[responseFilename],
|
||||
]
|
||||
: commonResponseLines[responseFilename];
|
||||
|
||||
await testPushBuildStream({
|
||||
builderMock: builder,
|
||||
commandLine: `push testApp -s ${projectPath} -R ${regSecretsPath} --gitignore`,
|
||||
commandLine: `push testApp -s ${projectPath} -R ${regSecretsPath} ${
|
||||
isV13() ? '' : '--gitignore'
|
||||
}`,
|
||||
expectedFiles,
|
||||
expectedQueryParams: commonQueryParams,
|
||||
expectedResponseLines,
|
||||
@ -445,7 +455,7 @@ describe('balena push', function () {
|
||||
'docker-compose.yml': { fileSize: 332, type: 'file' },
|
||||
'service1/Dockerfile.template': { fileSize: 144, type: 'file' },
|
||||
'service1/file1.sh': { fileSize: 12, type: 'file' },
|
||||
'service2/Dockerfile-alt': { fileSize: 40, type: 'file' },
|
||||
'service2/Dockerfile-alt': { fileSize: 13, type: 'file' },
|
||||
'service2/.dockerignore': { fileSize: 12, type: 'file' },
|
||||
'service2/file2-crlf.sh': {
|
||||
fileSize: isWindows ? 12 : 14,
|
||||
@ -498,7 +508,7 @@ describe('balena push', function () {
|
||||
'service1/Dockerfile.template': { fileSize: 144, type: 'file' },
|
||||
'service1/file1.sh': { fileSize: 12, type: 'file' },
|
||||
'service1/test-ignore.txt': { fileSize: 12, type: 'file' },
|
||||
'service2/Dockerfile-alt': { fileSize: 40, type: 'file' },
|
||||
'service2/Dockerfile-alt': { fileSize: 13, type: 'file' },
|
||||
'service2/.dockerignore': { fileSize: 12, type: 'file' },
|
||||
'service2/file2-crlf.sh': {
|
||||
fileSize: isWindows ? 12 : 14,
|
||||
@ -515,11 +525,11 @@ describe('balena push', function () {
|
||||
const expectedResponseLines: string[] = [
|
||||
...commonResponseLines[responseFilename],
|
||||
...[
|
||||
`[Info] ${hr}`,
|
||||
`[Info] ---------------------------------------------------------------------------`,
|
||||
'[Info] The --multi-dockerignore option is being used, and a .dockerignore file was',
|
||||
'[Info] found at the project source (root) directory. Note that this file will not',
|
||||
'[Info] be used to filter service subdirectories. See "balena help push".',
|
||||
`[Info] ${hr}`,
|
||||
`[Info] ---------------------------------------------------------------------------`,
|
||||
],
|
||||
];
|
||||
if (isWindows) {
|
||||
@ -570,7 +580,7 @@ describe('balena push: project validation', function () {
|
||||
];
|
||||
|
||||
const { out, err } = await runCommand(
|
||||
`push testApp --source ${projectPath} --gitignore`,
|
||||
`push testApp --source ${projectPath}`,
|
||||
);
|
||||
expect(cleanOutput(err, true)).to.include.members(expectedErrorLines);
|
||||
expect(out).to.be.empty;
|
||||
|
68
tests/commands/release.spec.ts
Normal file
68
tests/commands/release.spec.ts
Normal file
@ -0,0 +1,68 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2019-2020 Balena Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { expect } from 'chai';
|
||||
|
||||
import { BalenaAPIMock } from '../nock/balena-api-mock';
|
||||
import { cleanOutput, runCommand } from '../helpers';
|
||||
|
||||
describe('balena release', function () {
|
||||
let api: BalenaAPIMock;
|
||||
|
||||
beforeEach(() => {
|
||||
api = new BalenaAPIMock();
|
||||
api.expectGetWhoAmI({ optional: true, persist: true });
|
||||
api.expectGetMixpanel({ optional: true });
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
// Check all expected api calls have been made and clean up.
|
||||
api.done();
|
||||
});
|
||||
|
||||
it('should show release details', async () => {
|
||||
api.expectGetRelease();
|
||||
const { out } = await runCommand('release 27fda508c');
|
||||
const lines = cleanOutput(out);
|
||||
expect(lines[0]).to.contain('ID: ');
|
||||
expect(lines[0]).to.contain(' 142334');
|
||||
expect(lines[1]).to.contain('COMMIT: ');
|
||||
expect(lines[1]).to.contain(' 90247b54de4fa7a0a3cbc85e73c68039');
|
||||
});
|
||||
|
||||
it('should return release composition', async () => {
|
||||
api.expectGetRelease();
|
||||
const { out } = await runCommand('release 27fda508c --composition');
|
||||
const lines = cleanOutput(out);
|
||||
expect(lines[0]).to.be.equal("version: '2.1'");
|
||||
expect(lines[1]).to.be.equal('networks: {}');
|
||||
expect(lines[2]).to.be.equal('volumes:');
|
||||
expect(lines[3]).to.be.equal('resin-data: {}');
|
||||
expect(lines[4]).to.be.equal('services:');
|
||||
expect(lines[5]).to.be.equal('main:');
|
||||
});
|
||||
|
||||
it('should list releases', async () => {
|
||||
api.expectGetRelease();
|
||||
api.expectGetApplication();
|
||||
const { out } = await runCommand('releases someapp');
|
||||
const lines = cleanOutput(out);
|
||||
expect(lines.length).to.be.equal(2);
|
||||
expect(lines[1]).to.contain('142334');
|
||||
expect(lines[1]).to.contain('90247b54de4fa7a0a3cbc85e73c68039');
|
||||
});
|
||||
});
|
@ -19,7 +19,7 @@ import { expect } from 'chai';
|
||||
import mock = require('mock-require');
|
||||
import { createServer, Server } from 'net';
|
||||
|
||||
import { BalenaAPIMock } from '../balena-api-mock';
|
||||
import { BalenaAPIMock } from '../nock/balena-api-mock';
|
||||
import { cleanOutput, runCommand } from '../helpers';
|
||||
|
||||
// "itSS" means "it() Skip Standalone"
|
||||
|
@ -17,7 +17,7 @@
|
||||
import { expect } from 'chai';
|
||||
import * as fs from 'fs';
|
||||
|
||||
import { BalenaAPIMock } from '../balena-api-mock';
|
||||
import { BalenaAPIMock } from '../nock/balena-api-mock';
|
||||
import { runCommand } from '../helpers';
|
||||
|
||||
const packageJSON = JSON.parse(fs.readFileSync('./package.json', 'utf8'));
|
||||
|
@ -19,10 +19,20 @@ import { set as setEsVersion } from '@balena/es-version';
|
||||
// Set the desired es version for downstream modules that support it
|
||||
setEsVersion('es2018');
|
||||
|
||||
// Disable Sentry.io error reporting while running test code
|
||||
process.env.BALENARC_NO_SENTRY = '1';
|
||||
|
||||
// Disable deprecation checks while running test code
|
||||
// Like the global `--unsupported` flag
|
||||
process.env.BALENARC_UNSUPPORTED = '1';
|
||||
|
||||
import * as tmp from 'tmp';
|
||||
tmp.setGracefulCleanup();
|
||||
// Use a temporary dir for tests data
|
||||
process.env.BALENARC_DATA_DIRECTORY = tmp.dirSync().name;
|
||||
console.error(
|
||||
`[debug] tests/config-tests.ts: BALENARC_DATA_DIRECTORY="${process.env.BALENARC_DATA_DIRECTORY}"`,
|
||||
);
|
||||
|
||||
import { EventEmitter } from 'events';
|
||||
EventEmitter.defaultMaxListeners = 35; // it appears that 'nock' adds a bunch of listeners - bug?
|
||||
@ -33,3 +43,7 @@ import { config as chaiCfg } from 'chai';
|
||||
chaiCfg.showDiff = true;
|
||||
// enable diff comparison of large objects / arrays
|
||||
chaiCfg.truncateThreshold = 0;
|
||||
// Because mocks are pointed at "production", we need to make sure this is set to prod.
|
||||
// Otherwise if the user has BALENARC_BALENA_URL pointing at something else like staging, tests
|
||||
// will fail.
|
||||
process.env.BALENARC_BALENA_URL = 'balena-cloud.com';
|
||||
|
326
tests/deprecation.spec.ts
Normal file
326
tests/deprecation.spec.ts
Normal file
@ -0,0 +1,326 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2021 Balena Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import * as settings from 'balena-settings-client';
|
||||
import * as getStorage from 'balena-settings-storage';
|
||||
import { expect } from 'chai';
|
||||
import mock = require('mock-require');
|
||||
import * as semver from 'semver';
|
||||
import * as sinon from 'sinon';
|
||||
|
||||
import * as packageJSON from '../package.json';
|
||||
import {
|
||||
DeprecationChecker,
|
||||
ReleaseTimestampsByVersion,
|
||||
} from '../build/deprecation';
|
||||
import { BalenaAPIMock } from './nock/balena-api-mock';
|
||||
import { NpmMock } from './nock/npm-mock';
|
||||
import { runCommand, TestOutput } from './helpers';
|
||||
|
||||
// "itSS" means "it() Skip Standalone"
|
||||
const itSS = process.env.BALENA_CLI_TEST_TYPE === 'standalone' ? it.skip : it;
|
||||
|
||||
describe('DeprecationChecker', function () {
|
||||
const sandbox = sinon.createSandbox();
|
||||
const now = new Date().getTime();
|
||||
const anHourAgo = now - 3600000;
|
||||
const currentMajor = semver.major(packageJSON.version, { loose: true });
|
||||
const nextMajorVersion = `${currentMajor + 1}.0.0`;
|
||||
const dataDirectory = settings.get<string>('dataDirectory');
|
||||
const storageModPath = 'balena-settings-storage';
|
||||
const mockStorage = getStorage({ dataDirectory });
|
||||
let api: BalenaAPIMock;
|
||||
let npm: NpmMock;
|
||||
let checker: DeprecationChecker;
|
||||
let getStub: sinon.SinonStub<
|
||||
Parameters<typeof mockStorage.get>,
|
||||
ReturnType<typeof mockStorage.get>
|
||||
>;
|
||||
let setStub: sinon.SinonStub<
|
||||
Parameters<typeof mockStorage.set>,
|
||||
ReturnType<typeof mockStorage.set>
|
||||
>;
|
||||
let originalUnsupported: string | undefined;
|
||||
|
||||
this.beforeAll(() => {
|
||||
// Temporarily undo settings from `tests/config-tests.ts`
|
||||
originalUnsupported = process.env.BALENARC_UNSUPPORTED;
|
||||
delete process.env.BALENARC_UNSUPPORTED;
|
||||
});
|
||||
this.afterAll(() => {
|
||||
process.env.BALENARC_UNSUPPORTED = originalUnsupported;
|
||||
});
|
||||
|
||||
this.beforeEach(() => {
|
||||
npm = new NpmMock();
|
||||
api = new BalenaAPIMock();
|
||||
api.expectGetWhoAmI({ optional: true, persist: true });
|
||||
api.expectGetMixpanel({ optional: true });
|
||||
checker = new DeprecationChecker(packageJSON.version);
|
||||
|
||||
getStub = sandbox.stub(mockStorage, 'get').withArgs(checker.cacheFile);
|
||||
|
||||
setStub = sandbox
|
||||
.stub(mockStorage, 'set')
|
||||
.withArgs(checker.cacheFile, sinon.match.any);
|
||||
|
||||
mock(storageModPath, () => mockStorage);
|
||||
});
|
||||
|
||||
this.afterEach(() => {
|
||||
// Check all expected api calls have been made and clean up.
|
||||
(mockStorage.get as sinon.SinonStub).restore();
|
||||
(mockStorage.set as sinon.SinonStub).restore();
|
||||
|
||||
// originalStorage.set.restore();
|
||||
api.done();
|
||||
npm.done();
|
||||
mock.stop(storageModPath);
|
||||
});
|
||||
|
||||
itSS(
|
||||
'should warn if this version of the CLI is deprecated (isTTY is true)',
|
||||
async () => {
|
||||
const mockCache: ReleaseTimestampsByVersion = {
|
||||
lastFetched: '1970-01-01T00:00:00.000Z',
|
||||
};
|
||||
// pretend the next major was released just over half a year ago
|
||||
mockCache[nextMajorVersion] = new Date(
|
||||
checker.now - (checker.deprecationDays + 1) * checker.msInDay,
|
||||
).toISOString();
|
||||
|
||||
getStub.resolves(mockCache);
|
||||
|
||||
// Force isTTY to be true. It happens to be false (undefined) when
|
||||
// the tests run on balenaCI on Windows.
|
||||
const originalIsTTY = process.stderr.isTTY;
|
||||
process.stderr.isTTY = true;
|
||||
let result: TestOutput;
|
||||
try {
|
||||
result = await runCommand('version');
|
||||
} finally {
|
||||
process.stderr.isTTY = originalIsTTY;
|
||||
}
|
||||
const { out, err } = result;
|
||||
|
||||
expect(setStub.callCount).to.equal(0);
|
||||
expect(getStub.callCount).to.equal(1);
|
||||
expect(getStub.firstCall.args).to.deep.equal([checker.cacheFile]);
|
||||
|
||||
expect(out.join('')).to.equal(packageJSON.version + '\n');
|
||||
expect(err.join('')).to.equal(
|
||||
checker.getDeprecationMsg(checker.deprecationDays + 1) + '\n',
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
itSS(
|
||||
'should NOT warn if this version of the CLI is deprecated (isTTY is false)',
|
||||
async () => {
|
||||
const mockCache: ReleaseTimestampsByVersion = {
|
||||
lastFetched: '1970-01-01T00:00:00.000Z',
|
||||
};
|
||||
// pretend the next major was released just over half a year ago
|
||||
mockCache[nextMajorVersion] = new Date(
|
||||
checker.now - (checker.deprecationDays + 1) * checker.msInDay,
|
||||
).toISOString();
|
||||
|
||||
getStub.resolves(mockCache);
|
||||
|
||||
// Force isTTY to be false (undefined). It happens to be true when
|
||||
// the tests run on balenaCI on macOS and Linux.
|
||||
const originalIsTTY = process.stderr.isTTY;
|
||||
process.stderr.isTTY = undefined;
|
||||
let result: TestOutput;
|
||||
try {
|
||||
result = await runCommand('version');
|
||||
} finally {
|
||||
process.stderr.isTTY = originalIsTTY;
|
||||
}
|
||||
const { out, err } = result;
|
||||
|
||||
expect(setStub.callCount).to.equal(0);
|
||||
expect(getStub.callCount).to.equal(1);
|
||||
expect(getStub.firstCall.args).to.deep.equal([checker.cacheFile]);
|
||||
|
||||
expect(out.join('')).to.equal(packageJSON.version + '\n');
|
||||
expect(err.join('')).to.equal('');
|
||||
},
|
||||
);
|
||||
|
||||
itSS(
|
||||
'should NOT warn with --unsupported (deprecated but not expired)',
|
||||
async () => {
|
||||
const mockCache: ReleaseTimestampsByVersion = {
|
||||
lastFetched: '1970-01-01T00:00:00.000Z',
|
||||
};
|
||||
// pretend the next major was released just over half a year ago
|
||||
mockCache[nextMajorVersion] = new Date(
|
||||
checker.now - (checker.deprecationDays + 1) * checker.msInDay,
|
||||
).toISOString();
|
||||
|
||||
getStub.resolves(mockCache);
|
||||
|
||||
const { out, err } = await runCommand('version --unsupported');
|
||||
|
||||
expect(setStub.callCount).to.equal(0);
|
||||
expect(getStub.callCount).to.equal(0);
|
||||
|
||||
expect(out.join('')).to.equal(packageJSON.version + '\n');
|
||||
expect(err.join('')).to.be.empty;
|
||||
},
|
||||
);
|
||||
|
||||
itSS('should exit if this version of the CLI has expired', async () => {
|
||||
const mockCache: ReleaseTimestampsByVersion = {
|
||||
lastFetched: '1970-01-01T00:00:00.000Z',
|
||||
};
|
||||
// pretend the next major was released just over a year ago
|
||||
mockCache[nextMajorVersion] = new Date(
|
||||
checker.now - (checker.expiryDays + 1) * checker.msInDay,
|
||||
).toISOString();
|
||||
|
||||
getStub.resolves(mockCache);
|
||||
|
||||
const { out, err } = await runCommand('version');
|
||||
|
||||
expect(setStub.callCount).to.equal(0);
|
||||
expect(getStub.callCount).to.equal(1);
|
||||
expect(getStub.firstCall.args).to.deep.equal([checker.cacheFile]);
|
||||
|
||||
expect(out.join('')).to.equal('');
|
||||
expect(err.join('')).to.include(
|
||||
checker.getExpiryMsg(checker.expiryDays + 1) + '\n\n',
|
||||
);
|
||||
});
|
||||
|
||||
itSS('should NOT exit with --unsupported (expired version)', async () => {
|
||||
const mockCache: ReleaseTimestampsByVersion = {
|
||||
lastFetched: '1970-01-01T00:00:00.000Z',
|
||||
};
|
||||
// pretend the next major was released just over a year ago
|
||||
mockCache[nextMajorVersion] = new Date(
|
||||
checker.now - (checker.expiryDays + 1) * checker.msInDay,
|
||||
).toISOString();
|
||||
|
||||
getStub.resolves(mockCache);
|
||||
|
||||
const { out, err } = await runCommand('--unsupported version');
|
||||
|
||||
expect(setStub.callCount).to.equal(0);
|
||||
expect(getStub.callCount).to.equal(0);
|
||||
|
||||
expect(out.join('')).to.equal(packageJSON.version + '\n');
|
||||
expect(err.join('')).to.be.empty;
|
||||
});
|
||||
|
||||
it('should query the npm registry (empty cache file)', async () => {
|
||||
npm.expectGetBalenaCli({
|
||||
version: nextMajorVersion,
|
||||
publishedAt: new Date().toISOString(),
|
||||
});
|
||||
|
||||
getStub.resolves(undefined);
|
||||
|
||||
const { out, err } = await runCommand('version');
|
||||
|
||||
expect(setStub.callCount).to.equal(1);
|
||||
expect(setStub.firstCall.args.length).to.equal(2);
|
||||
const [name, obj] = setStub.firstCall.args;
|
||||
expect(name).to.equal(checker.cacheFile);
|
||||
expect(obj).to.have.property(nextMajorVersion);
|
||||
const lastFetched = new Date(obj[nextMajorVersion]).getTime();
|
||||
expect(lastFetched).to.be.greaterThan(anHourAgo);
|
||||
|
||||
expect(getStub.callCount).to.equal(1);
|
||||
expect(getStub.firstCall.args).to.deep.equal([checker.cacheFile]);
|
||||
|
||||
expect(out.join('')).to.equal(packageJSON.version + '\n');
|
||||
expect(err.join('')).to.equal('');
|
||||
});
|
||||
|
||||
it('should query the npm registry (not recently fetched)', async () => {
|
||||
npm.expectGetBalenaCli({
|
||||
version: nextMajorVersion,
|
||||
publishedAt: new Date().toISOString(),
|
||||
});
|
||||
|
||||
const mockCache: ReleaseTimestampsByVersion = {
|
||||
lastFetched: new Date(
|
||||
checker.now -
|
||||
(checker.majorVersionFetchIntervalDays + 1) * checker.msInDay,
|
||||
).toISOString(),
|
||||
};
|
||||
getStub.resolves(mockCache);
|
||||
|
||||
const { out, err } = await runCommand('version');
|
||||
|
||||
expect(setStub.callCount).to.equal(1);
|
||||
expect(setStub.firstCall.args.length).to.equal(2);
|
||||
const [name, obj] = setStub.firstCall.args;
|
||||
expect(name).to.equal(checker.cacheFile);
|
||||
expect(obj).to.have.property(nextMajorVersion);
|
||||
const lastFetched = new Date(obj[nextMajorVersion]).getTime();
|
||||
expect(lastFetched).to.be.greaterThan(anHourAgo);
|
||||
|
||||
expect(getStub.callCount).to.equal(1);
|
||||
expect(getStub.firstCall.args).to.deep.equal([checker.cacheFile]);
|
||||
|
||||
expect(out.join('')).to.equal(packageJSON.version + '\n');
|
||||
expect(err.join('')).to.equal('');
|
||||
});
|
||||
|
||||
itSS('should NOT query the npm registry (recently fetched)', async () => {
|
||||
const mockCache: ReleaseTimestampsByVersion = {
|
||||
lastFetched: new Date(
|
||||
checker.now -
|
||||
(checker.majorVersionFetchIntervalDays - 1) * checker.msInDay,
|
||||
).toISOString(),
|
||||
};
|
||||
getStub.resolves(mockCache);
|
||||
|
||||
const { out, err } = await runCommand('version');
|
||||
|
||||
expect(setStub.callCount).to.equal(0);
|
||||
expect(getStub.callCount).to.equal(1);
|
||||
expect(getStub.firstCall.args).to.deep.equal([checker.cacheFile]);
|
||||
|
||||
expect(out.join('')).to.equal(packageJSON.version + '\n');
|
||||
expect(err.join('')).to.equal('');
|
||||
});
|
||||
|
||||
itSS('should NOT query the npm registry (cached value)', async () => {
|
||||
const mockCache: ReleaseTimestampsByVersion = {
|
||||
lastFetched: '1970-01-01T00:00:00.000Z',
|
||||
};
|
||||
// pretend the next major was released just under half a year ago
|
||||
mockCache[nextMajorVersion] = new Date(
|
||||
checker.now - (checker.deprecationDays - 1) * checker.msInDay,
|
||||
).toISOString();
|
||||
|
||||
getStub.resolves(mockCache);
|
||||
|
||||
const { out, err } = await runCommand('version');
|
||||
|
||||
expect(setStub.callCount).to.equal(0);
|
||||
expect(getStub.callCount).to.equal(1);
|
||||
expect(getStub.firstCall.args).to.deep.equal([checker.cacheFile]);
|
||||
|
||||
expect(out.join('')).to.equal(packageJSON.version + '\n');
|
||||
expect(err.join('')).to.equal('');
|
||||
});
|
||||
});
|
@ -27,9 +27,10 @@ import * as tar from 'tar-stream';
|
||||
import { streamToBuffer } from 'tar-utils';
|
||||
import { URL } from 'url';
|
||||
|
||||
import { stripIndent } from '../lib/utils/lazy';
|
||||
import { BuilderMock } from './builder-mock';
|
||||
import { DockerMock } from './docker-mock';
|
||||
import { makeImageName } from '../build/utils/compose_ts';
|
||||
import { stripIndent } from '../build/utils/lazy';
|
||||
import { BuilderMock } from './nock/builder-mock';
|
||||
import { DockerMock } from './nock/docker-mock';
|
||||
import {
|
||||
cleanOutput,
|
||||
deepJsonParse,
|
||||
@ -161,22 +162,24 @@ export async function testDockerBuildStream(o: {
|
||||
expectedErrorLines?: string[];
|
||||
expectedExitCode?: number;
|
||||
expectedResponseLines: string[];
|
||||
projectName?: string; // --projectName command line flag
|
||||
projectPath: string;
|
||||
responseCode: number;
|
||||
responseBody: string;
|
||||
services: string[]; // e.g. ['main'] or ['service1', 'service2']
|
||||
tag?: string; // --tag command line flag
|
||||
}) {
|
||||
const expectedErrorLines = deepTemplateReplace(o.expectedErrorLines || [], o);
|
||||
const expectedResponseLines = deepTemplateReplace(o.expectedResponseLines, o);
|
||||
|
||||
for (const service of o.services) {
|
||||
// tagPrefix is, for example, 'myApp' if the path is 'path/to/myApp'
|
||||
const tagPrefix = o.projectPath.split(path.sep).pop();
|
||||
const tag = `${tagPrefix}_${service}`;
|
||||
const projectName = o.projectName || path.basename(o.projectPath);
|
||||
const tag = makeImageName(projectName, service, o.tag);
|
||||
const expectedFiles = o.expectedFilesByService[service];
|
||||
const expectedQueryParams = deepTemplateReplace(
|
||||
o.expectedQueryParamsByService[service],
|
||||
{ tag, ...o },
|
||||
{ ...o, tag },
|
||||
);
|
||||
const projectPath =
|
||||
service === 'main' ? o.projectPath : path.join(o.projectPath, service);
|
||||
@ -195,7 +198,7 @@ export async function testDockerBuildStream(o: {
|
||||
tag,
|
||||
});
|
||||
if (o.commandLine.startsWith('build')) {
|
||||
o.dockerMock.expectGetImages();
|
||||
o.dockerMock.expectGetImages({ optional: true });
|
||||
}
|
||||
}
|
||||
|
||||
|
122
tests/helpers.ts
122
tests/helpers.ts
@ -1,6 +1,6 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2019-2020 Balena Ltd.
|
||||
* Copyright 2019-2021 Balena Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@ -15,36 +15,59 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { execFile } from 'child_process';
|
||||
import intercept = require('intercept-stdout');
|
||||
import * as _ from 'lodash';
|
||||
import { promises as fs } from 'fs';
|
||||
import * as nock from 'nock';
|
||||
import * as path from 'path';
|
||||
|
||||
import * as balenaCLI from '../build/app';
|
||||
import * as packageJSON from '../package.json';
|
||||
|
||||
const balenaExe = process.platform === 'win32' ? 'balena.exe' : 'balena';
|
||||
const standalonePath = path.resolve(__dirname, '..', 'build-bin', balenaExe);
|
||||
|
||||
interface TestOutput {
|
||||
export interface TestOutput {
|
||||
err: string[]; // stderr
|
||||
out: string[]; // stdout
|
||||
exitCode?: number; // process.exitCode
|
||||
}
|
||||
|
||||
function matchesNodeEngineVersionWarn(msg: string) {
|
||||
if (/^-----+\r?\n?$/.test(msg)) {
|
||||
return true;
|
||||
}
|
||||
const cleanup = (line: string): string[] =>
|
||||
line
|
||||
.replace(/-----+/g, '')
|
||||
.replace(/"\d+\.\d+\.\d+"/, '"x.y.z"')
|
||||
.split(/\r?\n/)
|
||||
.map((l) => l.trim())
|
||||
.filter((l) => l);
|
||||
|
||||
const { getNodeEngineVersionWarn } = require('../build/utils/messages');
|
||||
let nodeEngineWarn: string = getNodeEngineVersionWarn(
|
||||
'x.y.z',
|
||||
packageJSON.engines.node,
|
||||
);
|
||||
const nodeEngineWarnArray = cleanup(nodeEngineWarn);
|
||||
nodeEngineWarn = nodeEngineWarnArray.join('\n');
|
||||
msg = cleanup(msg).join('\n');
|
||||
return msg === nodeEngineWarn || nodeEngineWarnArray.includes(msg);
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter stdout / stderr lines to remove lines that start with `[debug]` and
|
||||
* other lines that can be ignored for testing purposes.
|
||||
* @param testOutput
|
||||
*/
|
||||
function filterCliOutputForTests(testOutput: TestOutput): TestOutput {
|
||||
const { matchesNodeEngineVersionWarn } =
|
||||
require('../automation/utils') as typeof import('../automation/utils');
|
||||
export function filterCliOutputForTests({
|
||||
err,
|
||||
out,
|
||||
}: {
|
||||
err: string[];
|
||||
out: string[];
|
||||
}): { err: string[]; out: string[] } {
|
||||
return {
|
||||
exitCode: testOutput.exitCode,
|
||||
err: testOutput.err.filter(
|
||||
err: err.filter(
|
||||
(line: string) =>
|
||||
line &&
|
||||
!line.match(/\[debug\]/i) &&
|
||||
// TODO stop this warning message from appearing when running
|
||||
// sdk.setSharedOptions multiple times in the same process
|
||||
@ -52,7 +75,7 @@ function filterCliOutputForTests(testOutput: TestOutput): TestOutput {
|
||||
!line.startsWith('WARN: disabling Sentry.io error reporting') &&
|
||||
!matchesNodeEngineVersionWarn(line),
|
||||
),
|
||||
out: testOutput.out.filter((line: string) => !line.match(/\[debug\]/i)),
|
||||
out: out.filter((line: string) => line && !line.match(/\[debug\]/i)),
|
||||
};
|
||||
}
|
||||
|
||||
@ -61,6 +84,9 @@ function filterCliOutputForTests(testOutput: TestOutput): TestOutput {
|
||||
* @param cmd Command to execute, e.g. `push myApp` (without 'balena' prefix)
|
||||
*/
|
||||
async function runCommandInProcess(cmd: string): Promise<TestOutput> {
|
||||
const balenaCLI = await import('../build/app');
|
||||
const intercept = await import('intercept-stdout');
|
||||
|
||||
const preArgs = [process.argv[0], path.join(process.cwd(), 'bin', 'balena')];
|
||||
|
||||
const err: string[] = [];
|
||||
@ -79,18 +105,19 @@ async function runCommandInProcess(cmd: string): Promise<TestOutput> {
|
||||
const unhookIntercept = intercept(stdoutHook, stderrHook);
|
||||
|
||||
try {
|
||||
await balenaCLI.run(preArgs.concat(cmd.split(' ')), {
|
||||
await balenaCLI.run(preArgs.concat(cmd.split(' ').filter((c) => c)), {
|
||||
noFlush: true,
|
||||
});
|
||||
} finally {
|
||||
unhookIntercept();
|
||||
}
|
||||
return filterCliOutputForTests({
|
||||
err,
|
||||
out,
|
||||
const filtered = filterCliOutputForTests({ err, out });
|
||||
return {
|
||||
err: filtered.err,
|
||||
out: filtered.out,
|
||||
// this makes sense if `process.exit()` was stubbed with sinon
|
||||
exitCode: process.exitCode,
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
@ -129,10 +156,11 @@ async function runCommandInSubprocess(
|
||||
// override default proxy exclusion to allow proxying of requests to 127.0.0.1
|
||||
BALENARC_DO_PROXY: '127.0.0.1,localhost',
|
||||
};
|
||||
const { execFile } = await import('child_process');
|
||||
await new Promise<void>((resolve) => {
|
||||
const child = execFile(
|
||||
standalonePath,
|
||||
cmd.split(' '),
|
||||
cmd.split(' ').filter((c) => c),
|
||||
{ env: { ...process.env, ...addedEnvs } },
|
||||
($error, $stdout, $stderr) => {
|
||||
stderr = $stderr || '';
|
||||
@ -141,11 +169,12 @@ async function runCommandInSubprocess(
|
||||
// non-zero exit code. Usually this is harmless/expected, as
|
||||
// the CLI child process is tested for error conditions.
|
||||
if ($error && process.env.DEBUG) {
|
||||
console.error(`
|
||||
[debug] Error (possibly expected) executing child CLI process "${standalonePath}"
|
||||
------------------------------------------------------------------
|
||||
${$error}
|
||||
------------------------------------------------------------------`);
|
||||
const msg = `
|
||||
Error (possibly expected) executing child CLI process "${standalonePath}"
|
||||
${$error}`;
|
||||
const { warnify } =
|
||||
require('../build/utils/messages') as typeof import('../build/utils/messages');
|
||||
console.error(warnify(msg, '[debug] '));
|
||||
}
|
||||
resolve();
|
||||
},
|
||||
@ -166,11 +195,16 @@ ${$error}
|
||||
.filter((l) => l)
|
||||
.map((l) => l + '\n');
|
||||
|
||||
return filterCliOutputForTests({
|
||||
exitCode,
|
||||
const filtered = filterCliOutputForTests({
|
||||
err: splitLines(stderr),
|
||||
out: splitLines(stdout),
|
||||
});
|
||||
return {
|
||||
err: filtered.err,
|
||||
out: filtered.out,
|
||||
// this makes sense if `process.exit()` was stubbed with sinon
|
||||
exitCode,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
@ -190,11 +224,12 @@ export async function runCommand(cmd: string): Promise<TestOutput> {
|
||||
);
|
||||
}
|
||||
try {
|
||||
const { promises: fs } = await import('fs');
|
||||
await fs.access(standalonePath);
|
||||
} catch {
|
||||
throw new Error(`Standalone executable not found: "${standalonePath}"`);
|
||||
}
|
||||
const proxy = await import('./proxy-server');
|
||||
const proxy = await import('./nock/proxy-server');
|
||||
const [proxyPort] = await proxy.createProxyServerOnce();
|
||||
return runCommandInSubprocess(cmd, proxyPort);
|
||||
} else {
|
||||
@ -202,22 +237,6 @@ export async function runCommand(cmd: string): Promise<TestOutput> {
|
||||
}
|
||||
}
|
||||
|
||||
export const balenaAPIMock = () => {
|
||||
if (!nock.isActive()) {
|
||||
nock.activate();
|
||||
}
|
||||
|
||||
return nock(/./).get('/config/vars').reply(200, {
|
||||
reservedNames: [],
|
||||
reservedNamespaces: [],
|
||||
invalidRegex: '/^d|W/',
|
||||
whiteListedNames: [],
|
||||
whiteListedNamespaces: [],
|
||||
blackListedNames: [],
|
||||
configVarSchema: [],
|
||||
});
|
||||
};
|
||||
|
||||
export function cleanOutput(
|
||||
output: string[] | string,
|
||||
collapseBlank = false,
|
||||
@ -226,11 +245,17 @@ export function cleanOutput(
|
||||
? (line: string) => monochrome(line.trim()).replace(/\s{2,}/g, ' ')
|
||||
: (line: string) => monochrome(line.trim());
|
||||
|
||||
return _(_.castArray(output))
|
||||
.map((log: string) => log.split('\n').map(cleanLine))
|
||||
.flatten()
|
||||
.compact()
|
||||
.value();
|
||||
const result: string[] = [];
|
||||
output = typeof output === 'string' ? [output] : output;
|
||||
for (const lines of output) {
|
||||
for (let line of lines.split('\n')) {
|
||||
line = cleanLine(line);
|
||||
if (line) {
|
||||
result.push(line);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -320,6 +345,7 @@ export function deepJsonParse(data: any): any {
|
||||
export async function switchSentry(
|
||||
enabled: boolean | undefined,
|
||||
): Promise<boolean | undefined> {
|
||||
const balenaCLI = await import('../build/app');
|
||||
const sentryOpts = (await balenaCLI.setupSentry()).getClient()?.getOptions();
|
||||
if (sentryOpts) {
|
||||
const sentryStatus = sentryOpts.enabled;
|
||||
|
@ -21,7 +21,7 @@ import * as path from 'path';
|
||||
import { NockMock, ScopeOpts } from './nock-mock';
|
||||
|
||||
export const apiResponsePath = path.normalize(
|
||||
path.join(__dirname, 'test-data', 'api-response'),
|
||||
path.join(__dirname, '..', 'test-data', 'api-response'),
|
||||
);
|
||||
|
||||
const jHeader = { 'Content-Type': 'application/json' };
|
||||
@ -35,6 +35,7 @@ export class BalenaAPIMock extends NockMock {
|
||||
notFound = false,
|
||||
optional = false,
|
||||
persist = false,
|
||||
expandArchitecture = false,
|
||||
} = {}) {
|
||||
const interceptor = this.optGet(/^\/v6\/application($|[(?])/, {
|
||||
optional,
|
||||
@ -45,7 +46,12 @@ export class BalenaAPIMock extends NockMock {
|
||||
} else {
|
||||
interceptor.replyWithFile(
|
||||
200,
|
||||
path.join(apiResponsePath, 'application-GET-v6-expanded-app-type.json'),
|
||||
path.join(
|
||||
apiResponsePath,
|
||||
!expandArchitecture
|
||||
? 'application-GET-v6-expanded-app-type.json'
|
||||
: 'application-GET-v6-expanded-app-type-cpu-arch.json',
|
||||
),
|
||||
jHeader,
|
||||
);
|
||||
}
|
||||
@ -72,10 +78,10 @@ export class BalenaAPIMock extends NockMock {
|
||||
}
|
||||
|
||||
public expectApplicationProvisioning(opts: ScopeOpts = {}) {
|
||||
this.optPost(/^\/api-key\/application\/[0-9]+\/provisioning$/, opts).reply(
|
||||
200,
|
||||
'dummykey',
|
||||
);
|
||||
// The endpoint changed in balena-sdk v15.45.0:
|
||||
// before: '/api-key/application/${applicationId}/provisioning'
|
||||
// after: '/api-key/v1/'
|
||||
this.optPost(/^\/api-key\/v[0-9]\/?$/, opts).reply(200, 'dummykey');
|
||||
}
|
||||
|
||||
public expectGetMyApplication(opts: ScopeOpts = {}) {
|
||||
@ -95,12 +101,27 @@ export class BalenaAPIMock extends NockMock {
|
||||
});
|
||||
}
|
||||
|
||||
public expectGetRelease(opts: ScopeOpts = {}) {
|
||||
this.optGet(/^\/v6\/release($|[(?])/, opts).replyWithFile(
|
||||
200,
|
||||
path.join(apiResponsePath, 'release-GET-v6.json'),
|
||||
jHeader,
|
||||
);
|
||||
public expectGetRelease({
|
||||
notFound = false,
|
||||
optional = false,
|
||||
persist = false,
|
||||
} = {}) {
|
||||
const interceptor = this.optGet(/^\/v6\/release($|[(?])/, {
|
||||
persist,
|
||||
optional,
|
||||
});
|
||||
if (notFound) {
|
||||
interceptor.reply(200, { d: [] });
|
||||
} else {
|
||||
this.optGet(/^\/v6\/release($|[(?])/, {
|
||||
persist,
|
||||
optional,
|
||||
}).replyWithFile(
|
||||
200,
|
||||
path.join(apiResponsePath, 'release-GET-v6.json'),
|
||||
jHeader,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -122,11 +143,18 @@ export class BalenaAPIMock extends NockMock {
|
||||
/**
|
||||
* Mocks balena-release call
|
||||
*/
|
||||
public expectPostRelease(opts: ScopeOpts = {}) {
|
||||
this.optPost(/^\/v6\/release($|[(?])/, opts).replyWithFile(
|
||||
200,
|
||||
path.join(apiResponsePath, 'release-POST-v6.json'),
|
||||
jHeader,
|
||||
public expectPostRelease({
|
||||
statusCode = 200,
|
||||
inspectRequest = this.inspectNoOp,
|
||||
optional = false,
|
||||
persist = false,
|
||||
}) {
|
||||
this.optPost(/^\/v6\/release($|[(?])/, { optional, persist }).reply(
|
||||
statusCode,
|
||||
this.getInspectedReplyFileFunction(
|
||||
inspectRequest,
|
||||
path.join(apiResponsePath, 'release-POST-v6.json'),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@ -198,7 +226,7 @@ export class BalenaAPIMock extends NockMock {
|
||||
is_online: opts.isOnline,
|
||||
belongs_to__application: opts.inaccessibleApp
|
||||
? []
|
||||
: [{ app_name: 'test' }],
|
||||
: [{ app_name: 'test', slug: 'org/test' }],
|
||||
},
|
||||
],
|
||||
});
|
@ -22,7 +22,7 @@ import * as zlib from 'zlib';
|
||||
import { NockMock } from './nock-mock';
|
||||
|
||||
export const builderResponsePath = path.normalize(
|
||||
path.join(__dirname, 'test-data', 'builder-response'),
|
||||
path.join(__dirname, '..', 'test-data', 'builder-response'),
|
||||
);
|
||||
|
||||
export class BuilderMock extends NockMock {
|
@ -15,13 +15,13 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import * as _ from 'lodash';
|
||||
import * as path from 'path';
|
||||
import * as qs from 'querystring';
|
||||
|
||||
import { NockMock, ScopeOpts } from './nock-mock';
|
||||
|
||||
export const dockerResponsePath = path.normalize(
|
||||
path.join(__dirname, 'test-data', 'docker-response'),
|
||||
path.join(__dirname, '..', 'test-data', 'docker-response'),
|
||||
);
|
||||
|
||||
export class DockerMock extends NockMock {
|
||||
@ -78,7 +78,7 @@ export class DockerMock extends NockMock {
|
||||
checkBuildRequestBody: (requestBody: string) => Promise<void>;
|
||||
}) {
|
||||
this.optPost(
|
||||
new RegExp(`^/build\\?(|.+&)t=${_.escapeRegExp(opts.tag)}&`),
|
||||
new RegExp(`^/build\\?(|.+&)${qs.stringify({ t: opts.tag })}&`),
|
||||
opts,
|
||||
).reply(async function (uri, requestBody, cb) {
|
||||
let error: Error | null = null;
|
||||
@ -133,4 +133,42 @@ export class DockerMock extends NockMock {
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
public expectGetManifestBusybox(opts: ScopeOpts = {}) {
|
||||
// this.optGet(/^\/distribution\/.*/, opts).replyWithFile(
|
||||
this.optGet('/distribution/busybox/json', opts).replyWithFile(
|
||||
200,
|
||||
path.join(dockerResponsePath, 'distribution-busybox-GET.json'),
|
||||
{
|
||||
'api-version': '1.38',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
public expectGetManifestRpi3Alpine(opts: ScopeOpts = {}) {
|
||||
this.optGet(
|
||||
'/distribution/balenalib/raspberrypi3-alpine/json',
|
||||
opts,
|
||||
).replyWithFile(
|
||||
200,
|
||||
path.join(dockerResponsePath, 'distribution-rpi3alpine.json'),
|
||||
{
|
||||
'api-version': '1.38',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
public expectGetManifestNucAlpine(opts: ScopeOpts = {}) {
|
||||
// NOTE: This URL does no work in real life... it's "intel-nuc", not "nuc"
|
||||
this.optGet('/distribution/balenalib/nuc-alpine/json', opts).replyWithFile(
|
||||
200,
|
||||
path.join(dockerResponsePath, 'distribution-nucalpine.json'),
|
||||
{
|
||||
'api-version': '1.38',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
@ -16,6 +16,7 @@
|
||||
*/
|
||||
|
||||
import * as nock from 'nock';
|
||||
import * as fs from 'fs';
|
||||
|
||||
export interface ScopeOpts {
|
||||
optional?: boolean;
|
||||
@ -32,7 +33,10 @@ export class NockMock {
|
||||
public readonly expect;
|
||||
protected static instanceCount = 0;
|
||||
|
||||
constructor(public basePathPattern: string | RegExp) {
|
||||
constructor(
|
||||
public basePathPattern: string | RegExp,
|
||||
public allowUnmocked: boolean = false,
|
||||
) {
|
||||
if (NockMock.instanceCount === 0) {
|
||||
if (!nock.isActive()) {
|
||||
nock.activate();
|
||||
@ -44,7 +48,7 @@ export class NockMock {
|
||||
);
|
||||
}
|
||||
NockMock.instanceCount += 1;
|
||||
this.scope = nock(this.basePathPattern);
|
||||
this.scope = nock(this.basePathPattern, { allowUnmocked });
|
||||
this.expect = this.scope;
|
||||
}
|
||||
|
||||
@ -103,6 +107,27 @@ export class NockMock {
|
||||
};
|
||||
}
|
||||
|
||||
protected getInspectedReplyFileFunction(
|
||||
inspectRequest: (uri: string, requestBody: nock.Body) => void,
|
||||
replyBodyFile: string,
|
||||
) {
|
||||
return function (
|
||||
this: nock.ReplyFnContext,
|
||||
uri: string,
|
||||
requestBody: nock.Body,
|
||||
cb: (err: NodeJS.ErrnoException | null, result: nock.ReplyBody) => void,
|
||||
) {
|
||||
try {
|
||||
inspectRequest(uri, requestBody);
|
||||
} catch (err) {
|
||||
cb(err, '');
|
||||
}
|
||||
|
||||
const replyBody = fs.readFileSync(replyBodyFile);
|
||||
cb(null, replyBody);
|
||||
};
|
||||
}
|
||||
|
||||
public done() {
|
||||
try {
|
||||
// scope.done() will throw an error if there are expected api calls that have not happened.
|
50
tests/nock/npm-mock.ts
Normal file
50
tests/nock/npm-mock.ts
Normal file
@ -0,0 +1,50 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2020 Balena Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { NockMock } from './nock-mock';
|
||||
|
||||
const jHeader = { 'Content-Type': 'application/json' };
|
||||
|
||||
export class NpmMock extends NockMock {
|
||||
constructor() {
|
||||
super(/registry\.npmjs\.org/);
|
||||
}
|
||||
|
||||
public expectGetBalenaCli({
|
||||
version,
|
||||
publishedAt,
|
||||
notFound = false,
|
||||
optional = false,
|
||||
persist = false,
|
||||
}: {
|
||||
version: string;
|
||||
publishedAt: string;
|
||||
notFound?: boolean;
|
||||
optional?: boolean;
|
||||
persist?: boolean;
|
||||
}) {
|
||||
const interceptor = this.optGet(`/balena-cli/${version}`, {
|
||||
optional,
|
||||
persist,
|
||||
});
|
||||
if (notFound) {
|
||||
interceptor.reply(404, `version not found: ${version}`, jHeader);
|
||||
} else {
|
||||
interceptor.reply(200, { versionist: { publishedAt } }, jHeader);
|
||||
}
|
||||
}
|
||||
}
|
@ -22,7 +22,7 @@ import { Readable } from 'stream';
|
||||
import { NockMock, ScopeOpts } from './nock-mock';
|
||||
|
||||
export const dockerResponsePath = path.normalize(
|
||||
path.join(__dirname, 'test-data', 'docker-response'),
|
||||
path.join(__dirname, '..', 'test-data', 'docker-response'),
|
||||
);
|
||||
|
||||
export class SupervisorMock extends NockMock {
|
@ -86,7 +86,6 @@ export async function addRegSecretsEntries(
|
||||
|
||||
export function getDockerignoreWarn1(paths: string[], cmd: string) {
|
||||
const lines = [
|
||||
'[Warn] ----------------------------------------------------------------------',
|
||||
'[Warn] The following .dockerignore file(s) will not be used:',
|
||||
];
|
||||
lines.push(...paths.map((p) => `[Warn] * ${p}`));
|
||||
@ -96,7 +95,6 @@ export function getDockerignoreWarn1(paths: string[], cmd: string) {
|
||||
'[Warn] root) is used. Microservices (multicontainer) fleets may use a separate',
|
||||
'[Warn] .dockerignore file for each service with the --multi-dockerignore (-m)',
|
||||
`[Warn] option. See "balena help ${cmd}" for more details.`,
|
||||
'[Warn] ----------------------------------------------------------------------',
|
||||
],
|
||||
);
|
||||
return lines;
|
||||
@ -104,7 +102,6 @@ export function getDockerignoreWarn1(paths: string[], cmd: string) {
|
||||
|
||||
export function getDockerignoreWarn2(paths: string[], cmd: string) {
|
||||
const lines = [
|
||||
'[Warn] ----------------------------------------------------------------------',
|
||||
'[Warn] The following .dockerignore file(s) will not be used:',
|
||||
];
|
||||
lines.push(...paths.map((p) => `[Warn] * ${p}`));
|
||||
@ -114,7 +111,6 @@ export function getDockerignoreWarn2(paths: string[], cmd: string) {
|
||||
"[Warn] root of each service's build context (in a microservices/multicontainer",
|
||||
'[Warn] fleet), plus a .dockerignore file at the overall project root, are used.',
|
||||
`[Warn] See "balena help ${cmd}" for more details.`,
|
||||
'[Warn] ----------------------------------------------------------------------',
|
||||
],
|
||||
);
|
||||
return lines;
|
||||
|
@ -0,0 +1,53 @@
|
||||
{
|
||||
"d": [
|
||||
{
|
||||
"application_type": [
|
||||
{
|
||||
"name": "Starter",
|
||||
"slug": "microservices-starter",
|
||||
"supports_multicontainer": true,
|
||||
"is_legacy": false,
|
||||
"__metadata": {}
|
||||
}
|
||||
],
|
||||
"id": 1301645,
|
||||
"user": {
|
||||
"__deferred": {
|
||||
"uri": "/resin/user(43699)"
|
||||
},
|
||||
"__id": 43699
|
||||
},
|
||||
"organization": [
|
||||
{
|
||||
"handle": "gh_user"
|
||||
}
|
||||
],
|
||||
"depends_on__application": null,
|
||||
"actor": 3423895,
|
||||
"app_name": "testApp",
|
||||
"slug": "gh_user/testApp",
|
||||
"should_be__running_release": [
|
||||
{
|
||||
"commit": "96eec431d57e6976d3a756df33fde7e2"
|
||||
}
|
||||
],
|
||||
"is_for__device_type": [
|
||||
{
|
||||
"slug": "raspberrypi3",
|
||||
"is_of__cpu_architecture": [
|
||||
{
|
||||
"slug": "armv7hf"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"should_track_latest_release": true,
|
||||
"is_accessible_by_support_until__date": null,
|
||||
"is_public": false,
|
||||
"is_host": false,
|
||||
"__metadata": {
|
||||
"uri": "/resin/application(@id)?@id=1301645"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
@ -1,52 +1,95 @@
|
||||
{
|
||||
"d": [
|
||||
{
|
||||
"contains__image": [
|
||||
{
|
||||
"image": [
|
||||
{
|
||||
"id": 1820810,
|
||||
"created_at": "2020-01-04T01:13:08.805Z",
|
||||
"start_timestamp": "2020-01-04T01:13:08.583Z",
|
||||
"end_timestamp": "2020-01-04T01:13:11.920Z",
|
||||
"dockerfile": "# FROM busybox\n# FROM arm32v7/busybox\n# FROM arm32v7/alpine\n# FROM eu.gcr.io/buoyant-idea-226013/arm32v7/busybox\n# FROM eu.gcr.io/buoyant-idea-226013/amd64/busybox\n# FROM balenalib/raspberrypi3-debian:jessie-build\nFROM balenalib/raspberrypi3:stretch\nENV UDEV=1\n\n# FROM sander85/rpi-busybox # armv6\n# FROM balenalib/raspberrypi3-alpine\n\n# COPY start.sh /\n# COPY /src/start.sh /src/start.sh\n# COPY /src/hello.txt /\n# COPY src/hi.txt /\n\n# RUN cat /hello.txt\n# RUN cat /hi.txt\n# RUN cat /run/secrets/my-secret.txt\n# EXPOSE 80\nRUN uname -a\n\n# FROM alpine\n# RUN apk update && apk add bash\n# SHELL [\"/bin/bash\", \"-c\"]\n# CMD for ((i=1; i > 0; i++)); do echo \"(Plain Dockerfile 34-$i) $(uname -a)\"; sleep ${INTERVAL=5}; done\n\n# CMD i=1; while :; do echo \"Plain Dockerfile 36 ($i) $(uname -a)\"; sleep 10; i=$((i+1)); done\n# ENTRYPOINT [\"/usr/bin/entry.sh\"]\nCMD [\"/bin/bash\"]\n",
|
||||
"is_a_build_of__service": {
|
||||
"__deferred": {
|
||||
"uri": "/resin/service(233455)"
|
||||
},
|
||||
"__id": 233455
|
||||
},
|
||||
"image_size": 134320410,
|
||||
"is_stored_at__image_location": "registry2.balena-cloud.com/v2/9c00c9413942cd15cfc9189c5dac359d",
|
||||
"project_type": "Standard Dockerfile",
|
||||
"error_message": null,
|
||||
"build_log": "Step 1/4 : FROM balenalib/raspberrypi3:stretch\n ---> 8a75ea61d9c0\nStep 2/4 : ENV UDEV=1\n\u001b[42m\u001b[30mUsing cache\u001b[39m\u001b[49m\n ---> 159206067c8a\nStep 3/4 : RUN uname -a\n\u001b[42m\u001b[30mUsing cache\u001b[39m\u001b[49m\n ---> dd1b3d9c334b\nStep 4/4 : CMD [\"/bin/bash\"]\n\u001b[42m\u001b[30mUsing cache\u001b[39m\u001b[49m\n ---> 5211b6f4bb72\nSuccessfully built 5211b6f4bb72\n",
|
||||
"push_timestamp": "2020-01-04T01:13:14.415Z",
|
||||
"status": "success",
|
||||
"content_hash": "sha256:6b5471aae43ae81e8f69e10d1a516cb412569a6d5020a57eae311f8fa16d688a",
|
||||
"contract": null,
|
||||
"__metadata": {
|
||||
"uri": "/resin/image(@id)?@id=1820810"
|
||||
}
|
||||
}
|
||||
],
|
||||
"id": 1738663,
|
||||
"created_at": "2020-01-04T01:13:14.646Z",
|
||||
"is_part_of__release": {
|
||||
"__deferred": {
|
||||
"uri": "/resin/release(1203844)"
|
||||
},
|
||||
"__id": 1203844
|
||||
},
|
||||
"__metadata": {
|
||||
"uri": "/resin/image__is_part_of__release(@id)?@id=1738663"
|
||||
}
|
||||
{
|
||||
"id": 142334,
|
||||
"commit": "90247b54de4fa7a0a3cbc85e73c68039",
|
||||
"created_at": "2021-08-25T22:18:34.014Z",
|
||||
"status": "success",
|
||||
"semver": "0.0.0",
|
||||
"is_final": false,
|
||||
"build_log": null,
|
||||
"start_timestamp": "2021-08-25T22:18:33.624Z",
|
||||
"end_timestamp": "2021-08-25T22:18:48.820Z",
|
||||
"__metadata": {
|
||||
"uri": "/resin/release(@id)?@id=142334"
|
||||
},
|
||||
"contains__image": [
|
||||
{
|
||||
"image": [
|
||||
{
|
||||
"id": 1820810,
|
||||
"created_at": "2020-01-04T01:13:08.805Z",
|
||||
"start_timestamp": "2020-01-04T01:13:08.583Z",
|
||||
"end_timestamp": "2020-01-04T01:13:11.920Z",
|
||||
"dockerfile": "# FROM busybox\n# FROM arm32v7/busybox\n# FROM arm32v7/alpine\n# FROM eu.gcr.io/buoyant-idea-226013/arm32v7/busybox\n# FROM eu.gcr.io/buoyant-idea-226013/amd64/busybox\n# FROM balenalib/raspberrypi3-debian:jessie-build\nFROM balenalib/raspberrypi3:stretch\nENV UDEV=1\n\n# FROM sander85/rpi-busybox # armv6\n# FROM balenalib/raspberrypi3-alpine\n\n# COPY start.sh /\n# COPY /src/start.sh /src/start.sh\n# COPY /src/hello.txt /\n# COPY src/hi.txt /\n\n# RUN cat /hello.txt\n# RUN cat /hi.txt\n# RUN cat /run/secrets/my-secret.txt\n# EXPOSE 80\nRUN uname -a\n\n# FROM alpine\n# RUN apk update && apk add bash\n# SHELL [\"/bin/bash\", \"-c\"]\n# CMD for ((i=1; i > 0; i++)); do echo \"(Plain Dockerfile 34-$i) $(uname -a)\"; sleep ${INTERVAL=5}; done\n\n# CMD i=1; while :; do echo \"Plain Dockerfile 36 ($i) $(uname -a)\"; sleep 10; i=$((i+1)); done\n# ENTRYPOINT [\"/usr/bin/entry.sh\"]\nCMD [\"/bin/bash\"]\n",
|
||||
"is_a_build_of__service": {
|
||||
"__deferred": {
|
||||
"uri": "/resin/service(233455)"
|
||||
},
|
||||
"__id": 233455
|
||||
},
|
||||
"image_size": 134320410,
|
||||
"is_stored_at__image_location": "registry2.balena-cloud.com/v2/9c00c9413942cd15cfc9189c5dac359d",
|
||||
"project_type": "Standard Dockerfile",
|
||||
"error_message": null,
|
||||
"build_log": "Step 1/4 : FROM balenalib/raspberrypi3:stretch\n ---> 8a75ea61d9c0\nStep 2/4 : ENV UDEV=1\n\u001b[42m\u001b[30mUsing cache\u001b[39m\u001b[49m\n ---> 159206067c8a\nStep 3/4 : RUN uname -a\n\u001b[42m\u001b[30mUsing cache\u001b[39m\u001b[49m\n ---> dd1b3d9c334b\nStep 4/4 : CMD [\"/bin/bash\"]\n\u001b[42m\u001b[30mUsing cache\u001b[39m\u001b[49m\n ---> 5211b6f4bb72\nSuccessfully built 5211b6f4bb72\n",
|
||||
"push_timestamp": "2020-01-04T01:13:14.415Z",
|
||||
"status": "success",
|
||||
"content_hash": "sha256:6b5471aae43ae81e8f69e10d1a516cb412569a6d5020a57eae311f8fa16d688a",
|
||||
"contract": null,
|
||||
"__metadata": {
|
||||
"uri": "/resin/image(@id)?@id=1820810"
|
||||
}
|
||||
}
|
||||
],
|
||||
"id": 1203844,
|
||||
"id": 1738663,
|
||||
"created_at": "2020-01-04T01:13:14.646Z",
|
||||
"is_part_of__release": {
|
||||
"__deferred": {
|
||||
"uri": "/resin/release(1203844)"
|
||||
},
|
||||
"__id": 1203844
|
||||
},
|
||||
"__metadata": {
|
||||
"uri": "/resin/release(@id)?@id=1203844"
|
||||
"uri": "/resin/image__is_part_of__release(@id)?@id=1738663"
|
||||
}
|
||||
}
|
||||
],
|
||||
"release_tag": [
|
||||
{
|
||||
"tag_key": "testtag1",
|
||||
"value": "val1",
|
||||
"__metadata": {}
|
||||
}
|
||||
],
|
||||
"composition": {
|
||||
"version": "2.1",
|
||||
"networks": {},
|
||||
"volumes": {
|
||||
"resin-data": {}
|
||||
},
|
||||
"services": {
|
||||
"main": {
|
||||
"build": {
|
||||
"context": "."
|
||||
},
|
||||
"privileged": true,
|
||||
"tty": true,
|
||||
"restart": "always",
|
||||
"network_mode": "host",
|
||||
"volumes": [
|
||||
"resin-data:/data"
|
||||
],
|
||||
"labels": {
|
||||
"io.resin.features.kernel-modules": "1",
|
||||
"io.resin.features.firmware": "1",
|
||||
"io.resin.features.dbus": "1",
|
||||
"io.resin.features.supervisor-api": "1",
|
||||
"io.resin.features.resin-api": "1"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,53 @@
|
||||
{
|
||||
"Descriptor": {
|
||||
"mediaType": "application/vnd.docker.distribution.manifest.list.v2+json",
|
||||
"digest": "sha256:52f73a0a43a16cf37cd0720c90887ce972fe60ee06a687ee71fb93a7ca601df7",
|
||||
"size": 2295
|
||||
},
|
||||
"Platforms": [
|
||||
{
|
||||
"architecture": "amd64",
|
||||
"os": "linux"
|
||||
},
|
||||
{
|
||||
"architecture": "arm",
|
||||
"os": "linux",
|
||||
"variant": "v5"
|
||||
},
|
||||
{
|
||||
"architecture": "arm",
|
||||
"os": "linux",
|
||||
"variant": "v6"
|
||||
},
|
||||
{
|
||||
"architecture": "arm",
|
||||
"os": "linux",
|
||||
"variant": "v7"
|
||||
},
|
||||
{
|
||||
"architecture": "arm64",
|
||||
"os": "linux",
|
||||
"variant": "v8"
|
||||
},
|
||||
{
|
||||
"architecture": "386",
|
||||
"os": "linux"
|
||||
},
|
||||
{
|
||||
"architecture": "mips64le",
|
||||
"os": "linux"
|
||||
},
|
||||
{
|
||||
"architecture": "ppc64le",
|
||||
"os": "linux"
|
||||
},
|
||||
{
|
||||
"architecture": "riscv64",
|
||||
"os": "linux"
|
||||
},
|
||||
{
|
||||
"architecture": "s390x",
|
||||
"os": "linux"
|
||||
}
|
||||
]
|
||||
}
|
13
tests/test-data/docker-response/distribution-nucalpine.json
Normal file
13
tests/test-data/docker-response/distribution-nucalpine.json
Normal file
@ -0,0 +1,13 @@
|
||||
{
|
||||
"Descriptor": {
|
||||
"mediaType": "application/vnd.docker.distribution.manifest.v2+json",
|
||||
"digest": "sha256:d70bb0dd863198b41ea5d638993a9fbb912b3ea54b36480d1dc13e6b5b29021a",
|
||||
"size": 2610
|
||||
},
|
||||
"Platforms": [
|
||||
{
|
||||
"architecture": "amd64",
|
||||
"os": "linux"
|
||||
}
|
||||
]
|
||||
}
|
14
tests/test-data/docker-response/distribution-rpi3alpine.json
Normal file
14
tests/test-data/docker-response/distribution-rpi3alpine.json
Normal file
@ -0,0 +1,14 @@
|
||||
{
|
||||
"Descriptor": {
|
||||
"mediaType": "application/vnd.docker.distribution.manifest.v2+json",
|
||||
"digest": "sha256:2e33dc19d8514e01f7676532c507ddd95d0be20497fee25f4cbfc972cc6343d0",
|
||||
"size": 2821
|
||||
},
|
||||
"Platforms": [
|
||||
{
|
||||
"architecture": "arm",
|
||||
"os": "linux",
|
||||
"variant": "v7"
|
||||
}
|
||||
]
|
||||
}
|
@ -1,5 +1,3 @@
|
||||
> Warning Cannot find module 'net-keepalive' from 'build\utils\device'
|
||||
%1: build\utils\device\api.js
|
||||
> Warning Cannot resolve 'module'
|
||||
node_modules\balena-sync\build\index.js
|
||||
Dynamic require may fail at run time, because the requested file
|
||||
|
@ -1 +1 @@
|
||||
alternative Dockerfile (basic/service2)
|
||||
FROM busybox
|
||||
|
@ -0,0 +1,4 @@
|
||||
FROM busybox
|
||||
COPY ./src /usr/src/
|
||||
RUN chmod a+x /usr/src/*.sh
|
||||
CMD ["/usr/src/start.sh"]
|
@ -0,0 +1,3 @@
|
||||
name: testContract
|
||||
type: sw.application
|
||||
version: 1.5.2
|
2
tests/test-data/projects/no-docker-compose/with-contract/src/start.sh
Executable file
2
tests/test-data/projects/no-docker-compose/with-contract/src/start.sh
Executable file
@ -0,0 +1,2 @@
|
||||
#!/bin/sh
|
||||
echo "Hello, test!"
|
@ -7,6 +7,9 @@ import { FileIgnorer, IgnoreFileType } from '../../build/utils/ignore';
|
||||
// of the FileIgnorer class to prevent a Typescript compilation error (this
|
||||
// behavior is by design: see
|
||||
// https://github.com/microsoft/TypeScript/issues/19335 )
|
||||
//
|
||||
// v13: delete this file
|
||||
//
|
||||
describe('File ignorer', function () {
|
||||
it('should detect ignore files', function () {
|
||||
const f = new FileIgnorer(`.${path.sep}`);
|
||||
|
Reference in New Issue
Block a user