Compare commits

...

24 Commits

Author SHA1 Message Date
1f1af6d657 Add note about global flag in advanced install.
Change-type: patch
2024-01-05 17:54:25 -03:00
f46d00640b v17.4.10 2024-01-02 12:41:44 +00:00
e369bd3599 Merge pull request #2716 from balena-io/normalize-v-version-prefix
Normalize v prefixes in the --version parameter of all commands
2024-01-02 14:40:45 +02:00
75b29112a7 Deduplicate dependencies 2024-01-02 13:55:40 +02:00
b7b01ecd53 Normalize v prefixes in the --version parameter of all commands
Change-type: patch
2024-01-02 13:33:38 +02:00
801a25995c v17.4.9 2023-12-19 23:02:33 +00:00
8296dea78c Merge pull request #2713 from balena-io/fix-ci
Fix publishing artifacts to gh release
2023-12-19 23:01:35 +00:00
1da5a75c14 Fix publishing artifacts to gh release
Change-type: patch
2023-12-19 19:10:06 -03:00
166de57179 v17.4.8 2023-12-19 21:59:10 +00:00
85dece9e95 Merge pull request #2714 from balena-io/kyle/patch
Remove repo config from flowzone.yml
2023-12-19 21:58:24 +00:00
bfbc71215c Remove repo config from flowzone.yml
This functionality is being deprecated in Flowzone.

See: https://github.com/product-os/flowzone/pull/833

Change-type: patch
Signed-off-by: Kyle Harding <kyle@balena.io>
2023-12-19 15:20:04 -05:00
d243c14d74 v17.4.7 2023-12-19 14:26:31 +00:00
804eb27551 Merge pull request #2712 from balena-io/add-request-rerties-to-balena-deploy
deploy: Add rate-limiting aware retries for failed requests
2023-12-19 14:25:32 +00:00
4266dc6951 deploy: Add rate-limiting aware retries for failed requests
Change-type: patch
2023-12-19 01:16:42 +02:00
0ba3522584 Update dependencies
Update @balena/compose from 3.0.5 to 3.2.0

Also updates pinejs-client-request to support
using the Retry-After header and dockerode
to 3.3.5 to be aligned with @balena/compose.

Change-type: patch
2023-12-19 01:16:42 +02:00
19b0e9489d Deduplicate node modules 2023-12-19 01:16:42 +02:00
d9fed9c34c v17.4.6 2023-12-08 15:55:53 +00:00
81ee9f397f Merge pull request #2711 from balena-io/bump-oclif-core
Bump oclif core & use default missing flag handler
2023-12-08 15:54:47 +00:00
b9722c6796 Bump oclif core & use default missing flag handler
Change-type: patch
2023-12-08 12:06:54 -03:00
29ade0f696 v17.4.5 2023-12-04 14:08:31 +00:00
d5ae612513 Merge pull request #2707 from balena-io/bump-ts
Update TypeScript to 5.3.2
2023-12-04 14:07:30 +00:00
65ba63d1a8 Stop testing dependency deduplication on the custom test runners
That's since we already run that test as part of
flowzone's default "Test npm (18.x)", and the
custom tests are using the latest node & npm
version of the selected major.

Change-type: patch
2023-12-04 15:39:10 +02:00
f5ffa7d84f Temporarily pin oclif-core to ~3.11.0 to deduplicate the dependencies
Change-type: patch
2023-12-04 15:36:43 +02:00
dac3ace61d Update TypeScript to 5.3.2
Change-type: patch
2023-11-30 00:07:17 +02:00
23 changed files with 936 additions and 387 deletions

View File

@ -28,7 +28,7 @@ runs:
using: 'composite'
steps:
- name: Download custom source artifact
uses: actions/download-artifact@v3
uses: actions/download-artifact@f44cd7b40bfd40b6aa1cc1b9b5b7bf03d3c67110 # v4.1.0
with:
name: custom-${{ github.event.pull_request.head.sha || github.event.head_commit.id }}-${{ runner.os }}-${{ runner.arch }}
path: ${{ runner.temp }}
@ -127,8 +127,9 @@ runs:
XCODE_APP_LOADER_TEAM_ID: ${{ inputs.XCODE_APP_LOADER_TEAM_ID }}
- name: Upload artifacts
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@c7d193f32edcb7bfad88892161225aeda64e9392 # v4
with:
name: gh-release-${{ github.event.pull_request.head.sha || github.event.head_commit.id }}
name: gh-release-${{ github.event.pull_request.head.sha || github.event.head_commit.id }}-${{ strategy.job-index }}
path: dist
retention-days: 1
if-no-files-found: error

View File

@ -51,14 +51,14 @@ runs:
fi
npm run build
npm run test
npm run test:core
- name: Compress custom source
shell: pwsh
run: tar --exclude-vcs -acf ${{ runner.temp }}/custom.tgz .
- name: Upload custom artifact
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@c7d193f32edcb7bfad88892161225aeda64e9392 # v4
with:
name: custom-${{ github.event.pull_request.head.sha || github.event.head_commit.id }}-${{ runner.os }}-${{ runner.arch }}
path: ${{ runner.temp }}/custom.tgz

View File

@ -1,5 +1,4 @@
name: Flowzone
on:
pull_request:
types: [opened, synchronize, closed]
@ -7,7 +6,6 @@ on:
pull_request_target:
types: [opened, synchronize, closed]
branches: [main, master]
jobs:
flowzone:
name: Flowzone
@ -24,7 +22,5 @@ jobs:
secrets: inherit
with:
custom_runs_on: '[["self-hosted","Linux","distro:focal","X64"],["self-hosted","Linux","distro:focal","ARM64"],["macos-12"],["windows-2019"]]'
repo_config: true
repo_description: "The official balena CLI tool."
github_prerelease: false
restrict_custom_actions: false

View File

@ -1,3 +1,229 @@
- commits:
- subject: Normalize v prefixes in the --version parameter of all commands
hash: b7b01ecd5314bddae73b7b062f9d034b3661bcef
body: ""
footer:
Change-type: patch
change-type: patch
author: Thodoris Greasidis
nested: []
version: 17.4.10
title: ""
date: 2024-01-02T12:41:38.978Z
- commits:
- subject: Fix publishing artifacts to gh release
hash: 1da5a75c1411bdfece2b60f83095082f6ce68ace
body: ""
footer:
Change-type: patch
change-type: patch
author: Otávio Jacobi
nested: []
version: 17.4.9
title: ""
date: 2023-12-19T23:02:29.500Z
- commits:
- subject: Remove repo config from flowzone.yml
hash: bfbc71215c376e815e7d86561d87c5b697ba7482
body: |
This functionality is being deprecated in Flowzone.
See: https://github.com/product-os/flowzone/pull/833
footer:
Change-type: patch
change-type: patch
Signed-off-by: Kyle Harding <kyle@balena.io>
signed-off-by: Kyle Harding <kyle@balena.io>
author: Kyle Harding
nested: []
version: 17.4.8
title: ""
date: 2023-12-19T21:59:06.220Z
- commits:
- subject: "deploy: Add rate-limiting aware retries for failed requests"
hash: 4266dc69514c2177399fc605985196a436d75740
body: ""
footer:
Change-type: patch
change-type: patch
author: Thodoris Greasidis
nested: []
- subject: Update dependencies
hash: 0ba352258482048bbdb840be7ee9958b491f9b6c
body: |
Update @balena/compose from 3.0.5 to 3.2.0
Also updates pinejs-client-request to support
using the Retry-After header and dockerode
to 3.3.5 to be aligned with @balena/compose.
footer:
Change-type: patch
change-type: patch
author: Thodoris Greasidis
nested:
- commits:
- subject: 'release/createClient: Allow specifying the "retry" options'
hash: b89b42a838ed2c3a7a8319cbd1b2a7c66a8210ef
body: ""
footer:
Change-type: minor
change-type: minor
author: Thodoris Greasidis
nested: []
version: balena-compose-3.2.0
title: ""
date: 2023-12-05T15:26:57.394Z
- commits:
- subject: Update dockerode to 3.3.5
hash: f5fc932f3203df4df66d38363974e62788e468ff
body: ""
footer:
Change-type: patch
change-type: patch
author: Pagan Gazzard
nested: []
version: balena-compose-3.1.3
title: ""
date: 2023-11-29T14:49:55.816Z
- commits:
- subject: Use the JSONStream typings from @types/jsonstream
hash: 155fdcc8e4e7df67d41152b494e1a80493bb0439
body: ""
footer:
Change-type: patch
change-type: patch
author: Pagan Gazzard
nested: []
version: balena-compose-3.1.2
title: ""
date: 2023-11-29T13:33:49.557Z
- commits:
- subject: Make use of `pipeline` for piping streams together
hash: 1d98cd535a20fa67869da242b0ec7ddd713a4c7b
body: ""
footer:
Change-type: patch
change-type: patch
author: Pagan Gazzard
nested: []
version: balena-compose-3.1.1
title: ""
date: 2023-11-27T12:43:23.880Z
- commits:
- subject: Allow injecting any PinejsClientCore compatible API client
hash: e0ab3ef95f8bc51d2e9055a1f822b8d340f0c587
body: ""
footer:
Change-type: minor
change-type: minor
author: Thodoris Greasidis
nested: []
version: balena-compose-3.1.0
title: ""
date: 2023-11-13T16:27:44.317Z
- commits:
- subject: "NodeResolver: Refactor the recursion to an async-await loop"
hash: bde40f4430bc26a058598a64eeeedbb5ab35eb57
body: ""
footer:
Change-type: patch
change-type: patch
author: Thodoris Greasidis
nested: []
- subject: Drop bluebird & bluebird-lru-cache in favor of memoizee
hash: 82f90b210d73ff866f5d0546e73d8779db85a504
body: ""
footer:
Change-type: patch
change-type: patch
author: Thodoris Greasidis
nested: []
version: balena-compose-3.0.7
title: ""
date: 2023-11-10T16:10:01.859Z
- commits:
- subject: Fix the remaining linting errors
hash: 51b7893bc6156d0fa7a7821cc583032694ccda98
body: ""
footer:
Change-type: patch
change-type: patch
author: Thodoris Greasidis
nested: []
- subject: Remove unnecessary regex escaping
hash: 96b76abbcf78abd05157d49a5672a2621124bfe5
body: ""
footer:
Change-type: patch
change-type: patch
author: Thodoris Greasidis
nested: []
- subject: Replace the {} type with object
hash: dcf907ff124a638f591ae8e3fd80157eae1d1837
body: ""
footer:
Change-type: patch
change-type: patch
author: Thodoris Greasidis
nested: []
- subject: Update TypeScript to 5.2.2 and @blaena/lint to v7.2.1
hash: b583dd7ce8e964bef47f73dee53e08b7c1286532
body: ""
footer:
Change-type: patch
change-type: patch
author: Thodoris Greasidis
nested: []
version: balena-compose-3.0.6
title: ""
date: 2023-11-10T14:08:35.300Z
version: 17.4.7
title: ""
date: 2023-12-19T14:26:26.818Z
- commits:
- subject: Bump oclif core & use default missing flag handler
hash: b9722c67963c9b90e94aec7653ee488957ecd690
body: ""
footer:
Change-type: patch
change-type: patch
author: Otávio Jacobi
nested: []
version: 17.4.6
title: ""
date: 2023-12-08T15:55:47.078Z
- commits:
- subject: Stop testing dependency deduplication on the custom test runners
hash: 65ba63d1a8d231851634830be6d48fbf0e085e47
body: |
That's since we already run that test as part of
flowzone's default "Test npm (18.x)", and the
custom tests are using the latest node & npm
version of the selected major.
footer:
Change-type: patch
change-type: patch
author: Thodoris Greasidis
nested: []
- subject: Temporarily pin oclif-core to ~3.11.0 to deduplicate the dependencies
hash: f5ffa7d84f58047e1f262b2b1e1719fd4164d5de
body: ""
footer:
Change-type: patch
change-type: patch
author: Thodoris Greasidis
nested: []
- subject: Update TypeScript to 5.3.2
hash: dac3ace61d80dfd000a4a69dfa51d141d34ebc0a
body: ""
footer:
Change-type: patch
change-type: patch
author: Thodoris Greasidis
nested: []
version: 17.4.5
title: ""
date: 2023-12-04T14:08:25.597Z
- commits:
- subject: Fix balena block create to actually create a block
hash: b8769bb9e9dafac8a52eef477bc763551cf0d0b0

View File

@ -4,6 +4,70 @@ 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/).
## 17.4.10 - 2024-01-02
* Normalize v prefixes in the --version parameter of all commands [Thodoris Greasidis]
## 17.4.9 - 2023-12-19
* Fix publishing artifacts to gh release [Otávio Jacobi]
## 17.4.8 - 2023-12-19
* Remove repo config from flowzone.yml [Kyle Harding]
## 17.4.7 - 2023-12-19
* deploy: Add rate-limiting aware retries for failed requests [Thodoris Greasidis]
<details>
<summary> Update dependencies [Thodoris Greasidis] </summary>
> ### balena-compose-3.2.0 - 2023-12-05
>
> * release/createClient: Allow specifying the "retry" options [Thodoris Greasidis]
>
> ### balena-compose-3.1.3 - 2023-11-29
>
> * Update dockerode to 3.3.5 [Pagan Gazzard]
>
> ### balena-compose-3.1.2 - 2023-11-29
>
> * Use the JSONStream typings from @types/jsonstream [Pagan Gazzard]
>
> ### balena-compose-3.1.1 - 2023-11-27
>
> * Make use of `pipeline` for piping streams together [Pagan Gazzard]
>
> ### balena-compose-3.1.0 - 2023-11-13
>
> * Allow injecting any PinejsClientCore compatible API client [Thodoris Greasidis]
>
> ### balena-compose-3.0.7 - 2023-11-10
>
> * NodeResolver: Refactor the recursion to an async-await loop [Thodoris Greasidis]
> * Drop bluebird & bluebird-lru-cache in favor of memoizee [Thodoris Greasidis]
>
> ### balena-compose-3.0.6 - 2023-11-10
>
> * Fix the remaining linting errors [Thodoris Greasidis]
> * Remove unnecessary regex escaping [Thodoris Greasidis]
> * Replace the {} type with object [Thodoris Greasidis]
> * Update TypeScript to 5.2.2 and @blaena/lint to v7.2.1 [Thodoris Greasidis]
>
</details>
## 17.4.6 - 2023-12-08
* Bump oclif core & use default missing flag handler [Otávio Jacobi]
## 17.4.5 - 2023-12-04
* Stop testing dependency deduplication on the custom test runners [Thodoris Greasidis]
* Temporarily pin oclif-core to ~3.11.0 to deduplicate the dependencies [Thodoris Greasidis]
* Update TypeScript to 5.3.2 [Thodoris Greasidis]
## 17.4.4 - 2023-11-20
* Fix balena block create to actually create a block [Otávio Jacobi]

View File

@ -141,7 +141,8 @@ $ npm install balena-cli --global --production --unsafe-perm
```
`--unsafe-perm` is needed when `npm install` is executed as the `root` user (e.g. in a Docker
container) in order to allow npm scripts like `postinstall` to be executed.
container) in order to allow npm scripts like `postinstall` to be executed. The `--global` flag is needed so
the install uses the `npm-shrinkwrap.json` lockfile when [downloading dependencies](https://docs.npmjs.com/cli/v9/configuring-npm/npm-shrinkwrap-json#description).
## Additional Dependencies

View File

@ -259,6 +259,8 @@ export default class ConfigGenerateCmd extends Command {
if (!options.fleet && options.deviceType) {
throw new ExpectedError(this.deviceTypeNotAllowedMessage);
}
const { normalizeOsVersion } = await import('../../utils/normalization');
options.version = normalizeOsVersion(options.version);
const { validateDevOptionAndWarn } = await import('../../utils/config');
await validateDevOptionAndWarn(options.dev, options.version);
}

View File

@ -100,6 +100,8 @@ export default class DeviceOsUpdateCmd extends Command {
// Get target OS version
let targetOsVersion = options.version;
if (targetOsVersion != null) {
const { normalizeOsVersion } = await import('../../utils/normalization');
targetOsVersion = normalizeOsVersion(targetOsVersion);
if (!hupVersionInfo.versions.includes(targetOsVersion)) {
throw new ExpectedError(
`The provided version ${targetOsVersion} is not in the Host OS update targets for this device`,

View File

@ -216,9 +216,15 @@ export default class OsConfigureCmd extends Command {
configJson = JSON.parse(rawConfig);
}
const osVersion =
const { normalizeOsVersion } = await import('../../utils/normalization');
const osVersion = normalizeOsVersion(
options.version ||
(await getOsVersionFromImage(params.image, deviceTypeManifest, devInit));
(await getOsVersionFromImage(
params.image,
deviceTypeManifest,
devInit,
)),
);
const { validateDevOptionAndWarn } = await import('../../utils/config');
await validateDevOptionAndWarn(options.dev, osVersion);

View File

@ -211,7 +211,6 @@ const EXPECTED_ERROR_REGEXES = [
/^BalenaOrganizationNotFound/, // balena-sdk
/Request error: Unauthorized$/, // balena-sdk
/^Missing \d+ required arg/, // oclif parser: RequiredArgsError
/Missing required flag/, // oclif parser: RequiredFlagError
/^Unexpected argument/, // oclif parser: UnexpectedArgsError
/to be one of/, // oclif parser: FlagInvalidOptionError, ArgInvalidOptionError
/must also be provided when using/, // oclif parser (depends-on)

View File

@ -202,12 +202,8 @@ async function resolveOSVersion(
if (['menu', 'menu-esr'].includes(version)) {
return await selectOSVersionFromMenu(deviceType, version === 'menu-esr');
}
// Note that `version` may also be 'latest', 'recommended', 'default'
if (/^v?\d+\.\d+\.\d+/.test(version)) {
if (version[0] === 'v') {
version = version.slice(1);
}
}
const { normalizeOsVersion } = await import('./normalization');
version = normalizeOsVersion(version);
return version;
}

View File

@ -20,6 +20,7 @@ import type * as SDK from 'balena-sdk';
import type Dockerode = require('dockerode');
import * as path from 'path';
import type { Composition, ImageDescriptor } from '@balena/compose/dist/parse';
import type { RetryParametersObj } from 'pinejs-client-core';
import type {
BuiltImage,
ComposeOpts,
@ -94,22 +95,62 @@ export function createProject(
};
}
const getRequestRetryParameters = (): RetryParametersObj => {
if (
process.env.BALENA_CLI_TEST_TYPE != null &&
process.env.BALENA_CLI_TEST_TYPE !== ''
) {
// We only read the test env vars when in test mode.
const { intVar } =
require('@balena/env-parsing') as typeof import('@balena/env-parsing');
// We use the BALENARCTEST namespace and only parse the env vars while in test mode
// since we plan to switch all pinejs clients with the one of the SDK and might not
// want to have to support these env vars.
return {
minDelayMs: intVar('BALENARCTEST_API_RETRY_MIN_DELAY_MS'),
maxDelayMs: intVar('BALENARCTEST_API_RETRY_MAX_DELAY_MS'),
maxAttempts: intVar('BALENARCTEST_API_RETRY_MAX_ATTEMPTS'),
};
}
return {
minDelayMs: 1000,
maxDelayMs: 60000,
maxAttempts: 7,
};
};
export const createRelease = async function (
logger: Logger,
apiEndpoint: string,
auth: string,
userId: number,
appId: number,
composition: Composition,
draft: boolean,
semver?: string,
contract?: string,
semver: string | undefined,
contract: string | undefined,
): Promise<Release> {
const _ = require('lodash') as typeof import('lodash');
const crypto = require('crypto') as typeof import('crypto');
const releaseMod =
require('@balena/compose/dist/release') as typeof import('@balena/compose/dist/release');
const client = releaseMod.createClient({ apiEndpoint, auth });
const client = releaseMod.createClient({
apiEndpoint,
auth,
retry: {
...getRequestRetryParameters(),
onRetry: (err, delayMs, attempt, maxAttempts) => {
const code = err?.statusCode ?? 0;
logger.logDebug(
`API call failed with code ${code}. Attempting retry ${attempt} of ${maxAttempts} in ${
delayMs / 1000
} seconds`,
);
},
},
});
const { release, serviceImages } = await releaseMod.create({
client,

View File

@ -1385,6 +1385,7 @@ export async function deployProject(
`${prefix}Creating release...`,
() =>
createRelease(
logger,
apiEndpoint,
auth,
userId,

View File

@ -184,9 +184,9 @@ export async function validateDevOptionAndWarn(
* option.
*/
export async function validateSecureBootOptionAndWarn(
secureBoot?: boolean,
slug?: string,
version?: string,
secureBoot: boolean,
slug: string,
version: string,
logger?: import('./logger'),
) {
if (!secureBoot) {
@ -202,7 +202,7 @@ export async function validateSecureBootOptionAndWarn(
const sdk = getBalenaSdk();
const [osRelease] = await sdk.models.os.getAllOsVersions(slug, {
$select: 'contract',
$filter: { raw_version: `${version.replace(/^v/, '')}` },
$filter: { raw_version: version },
});
if (!osRelease) {
throw new ExpectedError(`Error: No ${version} release for ${slug}`);

View File

@ -81,3 +81,13 @@ export async function disambiguateReleaseParam(
export async function lowercaseIfSlug(s: string) {
return s.includes('/') ? s.toLowerCase() : s;
}
export function normalizeOsVersion(version: string) {
// Note that `version` may also be 'latest', 'recommended', 'default'
if (/^v?\d+\.\d+\.\d+/.test(version)) {
if (version[0] === 'v') {
version = version.slice(1);
}
}
return version;
}

727
npm-shrinkwrap.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
{
"name": "balena-cli",
"version": "17.4.4",
"version": "17.4.10",
"description": "The official balena Command Line Interface",
"main": "./build/app.js",
"homepage": "https://github.com/balena-io/balena-cli",
@ -59,7 +59,8 @@
"package": "npm run build:fast && npm run build:standalone && npm run build:installer",
"release": "ts-node --transpile-only automation/run.ts release",
"pretest": "npm run build",
"test": "npm run test:shrinkwrap && npm run test:source && npm run test:standalone",
"test": "npm run test:shrinkwrap && npm run test:core",
"test:core": "npm run test:source && npm run test:standalone",
"test:shrinkwrap": "ts-node --transpile-only automation/run.ts test-shrinkwrap",
"test:source": "cross-env BALENA_CLI_TEST_TYPE=source mocha",
"test:standalone": "npm run build:standalone && npm run test:standalone:fast",
@ -190,13 +191,14 @@
"simple-git": "^3.14.1",
"sinon": "^11.1.2",
"ts-node": "^10.4.0",
"typescript": "^5.1.3"
"typescript": "^5.3.2"
},
"dependencies": {
"@balena/compose": "^3.0.5",
"@balena/compose": "^3.2.0",
"@balena/dockerignore": "^1.0.2",
"@balena/env-parsing": "^1.1.8",
"@balena/es-version": "^1.0.1",
"@oclif/core": "^3.11.0",
"@oclif/core": "^3.14.1",
"@resin.io/valid-email": "^0.1.0",
"@sentry/node": "^6.16.1",
"@types/fast-levenshtein": "0.0.1",
@ -222,7 +224,7 @@
"denymount": "^2.3.0",
"docker-modem": "3.0.0",
"docker-progress": "^5.1.3",
"dockerode": "3.3.3",
"dockerode": "3.3.5",
"ejs": "^3.1.6",
"etcher-sdk": "^8.7.0",
"event-stream": "3.3.4",
@ -282,6 +284,6 @@
"windosu": "^0.3.0"
},
"versionist": {
"publishedAt": "2023-11-20T17:57:19.378Z"
"publishedAt": "2024-01-02T12:41:39.852Z"
}
}

View File

@ -12,10 +12,10 @@ index 607d8dc..07ba1f2 100644
return lines.join('\n');
}
diff --git a/node_modules/@oclif/core/lib/help/command.js b/node_modules/@oclif/core/lib/help/command.js
index c528e93..2c20760 100644
index 0753040..c1b0f67 100644
--- a/node_modules/@oclif/core/lib/help/command.js
+++ b/node_modules/@oclif/core/lib/help/command.js
@@ -45,7 +45,7 @@ class CommandHelp extends formatter_1.HelpFormatter {
@@ -58,7 +58,7 @@ class CommandHelp extends formatter_1.HelpFormatter {
if (args.filter((a) => a.description).length === 0)
return;
return args.map((a) => {
@ -23,9 +23,9 @@ index c528e93..2c20760 100644
+ const name = a.required ? `<${a.name}>` : `[${a.name}]`;
let description = a.description || '';
if (a.default)
description = `[default: ${a.default}] ${description}`;
@@ -137,14 +137,12 @@ class CommandHelp extends formatter_1.HelpFormatter {
label = labels.join(flag.char ? ', ' : ' ');
description = `${(0, theme_1.colorize)(this.config?.theme?.flagDefaultValue, `[default: ${a.default}]`)} ${description}`;
@@ -153,14 +153,12 @@ class CommandHelp extends formatter_1.HelpFormatter {
label = labels.join((0, theme_1.colorize)(this.config?.theme?.flagSeparator, flag.char ? ', ' : ' '));
}
if (flag.type === 'option') {
- let value = flag.helpValue || (this.opts.showFlagNameInTitle ? flag.name : '<value>');
@ -42,10 +42,10 @@ index c528e93..2c20760 100644
}
return label;
diff --git a/node_modules/@oclif/core/lib/help/index.js b/node_modules/@oclif/core/lib/help/index.js
index 38494f5..213b8b0 100644
index 242538a..efde8ac 100644
--- a/node_modules/@oclif/core/lib/help/index.js
+++ b/node_modules/@oclif/core/lib/help/index.js
@@ -158,11 +158,12 @@ class Help extends HelpBase {
@@ -168,11 +168,12 @@ class Help extends HelpBase {
}
this.log(this.formatCommand(command));
this.log('');
@ -56,12 +56,12 @@ index 38494f5..213b8b0 100644
this.log('');
}
- if (subCommands.length > 0) {
+ if (subCommands.length > 0 && !SUPPRESS_SUBTOPICS) {
+ if (subTopics.length > 0 && !SUPPRESS_SUBTOPICS) {
const aliases = [];
const uniqueSubCommands = subCommands.filter((p) => {
aliases.push(...p.aliases);
diff --git a/node_modules/@oclif/core/lib/parser/errors.js b/node_modules/@oclif/core/lib/parser/errors.js
index 51be624..768d589 100644
index 656ec6b..2bbf36b 100644
--- a/node_modules/@oclif/core/lib/parser/errors.js
+++ b/node_modules/@oclif/core/lib/parser/errors.js
@@ -14,7 +14,8 @@ Object.defineProperty(exports, "CLIError", { enumerable: true, get: function ()
@ -71,13 +71,13 @@ index 51be624..768d589 100644
- options.message += '\nSee more help with --help';
+ const help = options.command ? `\`${options.command} --help\`` : '--help';
+ options.message += `\nSee more help with ${help}`;
super(options.message);
super(options.message, { exit: options.exit });
this.parse = options.parse;
}
@@ -37,7 +38,8 @@ exports.InvalidArgsSpecError = InvalidArgsSpecError;
class RequiredArgsError extends CLIParseError {
args;
constructor({ args, flagsWithMultiple, parse, }) {
constructor({ args, exit, flagsWithMultiple, parse, }) {
- let message = `Missing ${args.length} required arg${args.length === 1 ? '' : 's'}`;
+ const command = 'balena ' + parse.input.context.id.replace(/:/g, ' ');
+ let message = `Missing ${args.length} required argument${args.length === 1 ? '' : 's'}`;
@ -88,20 +88,8 @@ index 51be624..768d589 100644
message += `\n\nNote: ${flags} allow${flagsWithMultiple.length === 1 ? 's' : ''} multiple values. Because of this you need to provide all arguments before providing ${flagsWithMultiple.length === 1 ? 'that flag' : 'those flags'}.`;
message += '\nAlternatively, you can use "--" to signify the end of the flags and the beginning of arguments.';
}
- super({ message, parse });
+ super({ message, parse, command });
- super({ exit: cache_1.default.getInstance().get('exitCodes')?.requiredArgs ?? exit, message, parse });
+ super({ exit: cache_1.default.getInstance().get('exitCodes')?.requiredArgs ?? exit, message, parse, command });
this.args = args;
}
}
@@ -56,9 +58,10 @@ exports.RequiredArgsError = RequiredArgsError;
class RequiredFlagError extends CLIParseError {
flag;
constructor({ flag, parse }) {
+ const command = 'balena ' + parse.input.context.id.replace(/:/g, ' ');
const usage = (0, list_1.renderList)((0, help_1.flagUsages)([flag], { displayRequired: false }));
const message = `Missing required flag:\n${usage}`;
- super({ message, parse });
+ super({ message, parse, command });
this.flag = flag;
}
}

View File

@ -15,6 +15,7 @@
* limitations under the License.
*/
import { intVar } from '@balena/env-parsing';
import type { Request as ReleaseRequest } from '@balena/compose/dist/release';
import { expect } from 'chai';
import { promises as fs } from 'fs';
@ -284,16 +285,25 @@ describe('balena deploy', function () {
api.expectPostRelease({});
docker.expectGetManifestBusybox();
let failedImagePatchRequests = 0;
// Mock this patch HTTP request to return status code 500, in which case
// the release status should be saved as "failed" rather than "success"
const maxRequestRetries = intVar('BALENARCTEST_API_RETRY_MAX_ATTEMPTS');
expect(
maxRequestRetries,
'BALENARCTEST_API_RETRY_MAX_ATTEMPTS must be >= 2 for this test',
).to.be.greaterThanOrEqual(2);
api.expectPatchImage({
replyBody: errMsg,
statusCode: 500,
// b/c failed requests are retried
times: maxRequestRetries,
inspectRequest: (_uri, requestBody) => {
const imageBody = requestBody as Partial<
import('@balena/compose/dist/release/models').ImageModel
>;
expect(imageBody.status).to.equal('success');
failedImagePatchRequests++;
},
});
// Check that the CLI patches the release with status="failed"
@ -324,6 +334,7 @@ describe('balena deploy', function () {
responseCode: 200,
services: ['main'],
});
expect(failedImagePatchRequests).to.equal(maxRequestRetries);
} finally {
await switchSentry(sentryStatus);
// @ts-expect-error claims restore does not exist
@ -331,6 +342,82 @@ describe('balena deploy', function () {
}
});
it('should create the expected --build tar stream after retrying failing OData requests (single container)', async () => {
const projectPath = path.join(projectsPath, 'no-docker-compose', 'basic');
const expectedFiles: ExpectedTarStreamFiles = {
'src/.dockerignore': { fileSize: 16, type: 'file' },
'src/start.sh': { fileSize: 89, type: 'file' },
'src/windows-crlf.sh': {
fileSize: isWindows ? 68 : 70,
testStream: isWindows ? expectStreamNoCRLF : undefined,
type: 'file',
},
Dockerfile: { fileSize: 88, type: 'file' },
'Dockerfile-alt': { fileSize: 30, 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}"`,
...getDockerignoreWarn1(
[path.join(projectPath, 'src', '.dockerignore')],
'deploy',
),
];
if (isWindows) {
const fname = path.join(projectPath, 'src', 'windows-crlf.sh');
expectedResponseLines.push(
`[Info] Converting line endings CRLF -> LF for file: ${fname}`,
);
}
api.expectPostRelease({});
docker.expectGetManifestBusybox();
const maxRequestRetries = intVar('BALENARCTEST_API_RETRY_MAX_ATTEMPTS');
expect(
maxRequestRetries,
'BALENARCTEST_API_RETRY_MAX_ATTEMPTS must be >= 2 for this test',
).to.be.greaterThanOrEqual(2);
let failedImagePatchRequests = 0;
let succesfullImagePatchRequests = 0;
api
.optPatch(/^\/v6\/image($|[(?])/, { times: maxRequestRetries })
.reply((_uri, requestBody) => {
const imageBody = requestBody as Partial<
import('@balena/compose/dist/release/models').ImageModel
>;
expect(imageBody.status).to.equal('success');
if (failedImagePatchRequests < maxRequestRetries - 1) {
failedImagePatchRequests++;
return [500, 'Patch Image Error'];
}
succesfullImagePatchRequests++;
return [200, 'OK'];
});
api.expectPatchRelease({});
api.expectPostImageLabel();
await testDockerBuildStream({
commandLine: `deploy testApp --build --source ${projectPath}`,
dockerMock: docker,
expectedFilesByService: { main: expectedFiles },
expectedQueryParamsByService: { main: commonQueryParams },
expectedResponseLines,
projectPath,
responseBody,
responseCode: 200,
services: ['main'],
});
expect(failedImagePatchRequests).to.equal(maxRequestRetries - 1);
expect(succesfullImagePatchRequests).to.equal(1);
});
it('should create the expected tar stream (docker-compose, --multi-dockerignore)', async () => {
const projectPath = path.join(projectsPath, 'docker-compose', 'basic');
const service1Dockerfile = (

View File

@ -26,6 +26,11 @@ process.env.BALENARC_NO_SENTRY = '1';
// Like the global `--unsupported` flag
process.env.BALENARC_UNSUPPORTED = '1';
// Reduce the api request retry limits to keep the tests fast.
process.env.BALENARCTEST_API_RETRY_MIN_DELAY_MS = '100';
process.env.BALENARCTEST_API_RETRY_MAX_DELAY_MS = '1000';
process.env.BALENARCTEST_API_RETRY_MAX_ATTEMPTS = '2';
import * as tmp from 'tmp';
tmp.setGracefulCleanup();
// Use a temporary dir for tests data

View File

@ -131,7 +131,6 @@ describe('handleError() function', () => {
const messagesToMatch = [
'Missing 1 required argument', // oclif
'Missing 2 required arguments', // oclif
'Missing required flag', // oclif
'Unexpected argument', // oclif
'Unexpected arguments', // oclif
'to be one of', // oclif

View File

@ -35,11 +35,13 @@ export class BalenaAPIMock extends NockMock {
notFound = false,
optional = false,
persist = false,
times = undefined as number | undefined,
expandArchitecture = false,
} = {}) {
const interceptor = this.optGet(/^\/v6\/application($|[(?])/, {
optional,
persist,
times,
});
if (notFound) {
interceptor.reply(200, { d: [] });
@ -105,10 +107,12 @@ export class BalenaAPIMock extends NockMock {
notFound = false,
optional = false,
persist = false,
times = undefined as number | undefined,
} = {}) {
const interceptor = this.optGet(/^\/v6\/release($|[(?])/, {
persist,
optional,
times,
});
if (notFound) {
interceptor.reply(200, { d: [] });
@ -133,8 +137,9 @@ export class BalenaAPIMock extends NockMock {
inspectRequest = this.inspectNoOp,
optional = false,
persist = false,
times = undefined as number | undefined,
}) {
this.optPatch(/^\/v6\/release($|[(?])/, { optional, persist }).reply(
this.optPatch(/^\/v6\/release($|[(?])/, { optional, persist, times }).reply(
statusCode,
this.getInspectedReplyBodyFunction(inspectRequest, replyBody),
);
@ -148,8 +153,9 @@ export class BalenaAPIMock extends NockMock {
inspectRequest = this.inspectNoOp,
optional = false,
persist = false,
times = undefined as number | undefined,
}) {
this.optPost(/^\/v6\/release($|[(?])/, { optional, persist }).reply(
this.optPost(/^\/v6\/release($|[(?])/, { optional, persist, times }).reply(
statusCode,
this.getInspectedReplyFileFunction(
inspectRequest,
@ -167,8 +173,9 @@ export class BalenaAPIMock extends NockMock {
inspectRequest = this.inspectNoOp,
optional = false,
persist = false,
times = undefined as number | undefined,
}) {
this.optPatch(/^\/v6\/image($|[(?])/, { optional, persist }).reply(
this.optPatch(/^\/v6\/image($|[(?])/, { optional, persist, times }).reply(
statusCode,
this.getInspectedReplyBodyFunction(inspectRequest, replyBody),
);

View File

@ -21,6 +21,7 @@ import * as fs from 'fs';
export interface ScopeOpts {
optional?: boolean;
persist?: boolean;
times?: number;
}
/**
@ -52,36 +53,50 @@ export class NockMock {
this.expect = this.scope;
}
public optMethod(
method: 'get' | 'delete' | 'patch' | 'post',
uri: string | RegExp | ((uri: string) => boolean),
{ optional = false, persist = false, times = undefined }: ScopeOpts,
) {
let scope = this.scope;
if (persist) {
scope = scope.persist();
}
let reqInterceptor = scope[method](uri);
if (times != null) {
reqInterceptor = reqInterceptor.times(times);
} else if (optional) {
reqInterceptor = reqInterceptor.optionally();
}
return reqInterceptor;
}
public optGet(
uri: string | RegExp | ((uri: string) => boolean),
{ optional = false, persist = false }: ScopeOpts,
opts: ScopeOpts,
): nock.Interceptor {
const get = (persist ? this.scope.persist() : this.scope).get(uri);
return optional ? get.optionally() : get;
return this.optMethod('get', uri, opts);
}
public optDelete(
uri: string | RegExp | ((uri: string) => boolean),
{ optional = false, persist = false }: ScopeOpts,
opts: ScopeOpts,
) {
const del = (persist ? this.scope.persist() : this.scope).delete(uri);
return optional ? del.optionally() : del;
return this.optMethod('delete', uri, opts);
}
public optPatch(
uri: string | RegExp | ((uri: string) => boolean),
{ optional = false, persist = false }: ScopeOpts,
opts: ScopeOpts,
) {
const patch = (persist ? this.scope.persist() : this.scope).patch(uri);
return optional ? patch.optionally() : patch;
return this.optMethod('patch', uri, opts);
}
public optPost(
uri: string | RegExp | ((uri: string) => boolean),
{ optional = false, persist = false }: ScopeOpts,
opts: ScopeOpts,
) {
const post = (persist ? this.scope.persist() : this.scope).post(uri);
return optional ? post.optionally() : post;
return this.optMethod('post', uri, opts);
}
protected inspectNoOp(_uri: string, _requestBody: nock.Body): void {