mirror of
https://github.com/balena-io/balena-cli.git
synced 2025-06-24 18:45:07 +00:00
Compare commits
53 Commits
example-mo
...
v19.0.17
Author | SHA1 | Date | |
---|---|---|---|
77ccd9c39c | |||
9e140eff13 | |||
da95baa70c | |||
a3ec75c2c7 | |||
f6f6be8ee8 | |||
09e653692b | |||
3ac89b236a | |||
bd472f2380 | |||
b5dcf45c40 | |||
7e2b5abe60 | |||
7b66e0d216 | |||
877c5031a4 | |||
1245b1c99b | |||
8dbe1af551 | |||
aae303202b | |||
284784505d | |||
77b9514442 | |||
ff4afe3ab2 | |||
5ea246f016 | |||
127bd7ec72 | |||
fa35877137 | |||
a402dffbc5 | |||
c7441b06ac | |||
251d64eb88 | |||
ff9bb52a20 | |||
c799c3f10d | |||
89efe2a2c8 | |||
f6ff397969 | |||
aaf709a1d4 | |||
ca6eea4371 | |||
d39dc5a39a | |||
1699419788 | |||
c25591cb4a | |||
a2b4f76c94 | |||
6a1239bd52 | |||
ddf34326a4 | |||
58f480ad7c | |||
7e6589a7d7 | |||
c699bb1dbc | |||
e101e0f466 | |||
e29273142e | |||
519395cfcd | |||
314e8800d0 | |||
0bb1c892e8 | |||
5eb79f5cf0 | |||
707b249e97 | |||
2a725cd1f0 | |||
83f274cc62 | |||
9242a3493a | |||
aa46d314b4 | |||
58f7dfc894 | |||
39e1c02648 | |||
5f92bbc846 |
8
.github/actions/publish/action.yml
vendored
8
.github/actions/publish/action.yml
vendored
@ -28,7 +28,7 @@ runs:
|
||||
using: 'composite'
|
||||
steps:
|
||||
- name: Download custom source artifact
|
||||
uses: actions/download-artifact@65a9edc5881444af0b9093a5e628f2fe47ea3b2e # v4.1.7
|
||||
uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8
|
||||
with:
|
||||
name: custom-${{ github.event.pull_request.head.sha || github.event.head_commit.id }}-${{ runner.os }}-${{ runner.arch }}
|
||||
path: ${{ runner.temp }}
|
||||
@ -39,7 +39,7 @@ runs:
|
||||
run: tar -xf ${{ runner.temp }}/custom.tgz
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4
|
||||
uses: actions/setup-node@1e60f620b9541d16bece96c5465dc8ee9832be0b # v4
|
||||
with:
|
||||
node-version: ${{ inputs.NODE_VERSION }}
|
||||
cache: npm
|
||||
@ -66,7 +66,7 @@ runs:
|
||||
# https://github.com/Apple-Actions/import-codesign-certs
|
||||
- name: Import Apple code signing certificate
|
||||
if: runner.os == 'macOS'
|
||||
uses: apple-actions/import-codesign-certs@253ddeeac23f2bdad1646faac5c8c2832e800071 # v1
|
||||
uses: apple-actions/import-codesign-certs@8f3fb608891dd2244cdab3d69cd68c0d37a7fe93 # v2
|
||||
with:
|
||||
p12-file-base64: ${{ fromJSON(inputs.secrets).APPLE_SIGNING }}
|
||||
p12-password: ${{ fromJSON(inputs.secrets).APPLE_SIGNING_PASSWORD }}
|
||||
@ -135,7 +135,7 @@ runs:
|
||||
XCODE_APP_LOADER_TEAM_ID: ${{ inputs.XCODE_APP_LOADER_TEAM_ID }}
|
||||
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4
|
||||
uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4
|
||||
with:
|
||||
name: gh-release-${{ github.event.pull_request.head.sha || github.event.head_commit.id }}-${{ strategy.job-index }}
|
||||
path: dist
|
||||
|
4
.github/actions/test/action.yml
vendored
4
.github/actions/test/action.yml
vendored
@ -26,7 +26,7 @@ runs:
|
||||
steps:
|
||||
# https://github.com/actions/setup-node#caching-global-packages-data
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4
|
||||
uses: actions/setup-node@1e60f620b9541d16bece96c5465dc8ee9832be0b # v4
|
||||
with:
|
||||
node-version: ${{ inputs.NODE_VERSION }}
|
||||
cache: npm
|
||||
@ -58,7 +58,7 @@ runs:
|
||||
run: tar --exclude-vcs -acf ${{ runner.temp }}/custom.tgz .
|
||||
|
||||
- name: Upload custom artifact
|
||||
uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4
|
||||
uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4
|
||||
with:
|
||||
name: custom-${{ github.event.pull_request.head.sha || github.event.head_commit.id }}-${{ runner.os }}-${{ runner.arch }}
|
||||
path: ${{ runner.temp }}/custom.tgz
|
||||
|
@ -1,3 +1,231 @@
|
||||
- commits:
|
||||
- subject: Remove dev dependency `parse-link-header`
|
||||
hash: da95baa70cc2dc72e6d529fa25c42cd2e1739c10
|
||||
body: ""
|
||||
footer:
|
||||
Change-type: patch
|
||||
change-type: patch
|
||||
author: myarmolinsky
|
||||
nested: []
|
||||
- subject: Remove dev dependency @octokit/rest
|
||||
hash: a3ec75c2c752f734f07e84e9213887b4aec4e7fd
|
||||
body: ""
|
||||
footer:
|
||||
Change-type: patch
|
||||
change-type: patch
|
||||
author: myarmolinsky
|
||||
nested: []
|
||||
- subject: Remove dev dependency @octokit/plugin-throttling
|
||||
hash: f6f6be8ee8be80048e621a4e75d2fbecacce47e4
|
||||
body: ""
|
||||
footer:
|
||||
Change-type: patch
|
||||
change-type: patch
|
||||
author: myarmolinsky
|
||||
nested: []
|
||||
- subject: Remove no longer needed references and tests for mixpanel
|
||||
hash: 09e653692b00777aa56625751110305223bc5917
|
||||
body: ""
|
||||
footer:
|
||||
Change-type: patch
|
||||
change-type: patch
|
||||
author: myarmolinsky
|
||||
nested: []
|
||||
- subject: Remove dev dependency `@types/mixpanel`
|
||||
hash: 3ac89b236abe3392c185010c3b3851a6923d083a
|
||||
body: ""
|
||||
footer:
|
||||
Change-type: patch
|
||||
change-type: patch
|
||||
author: myarmolinsky
|
||||
nested: []
|
||||
version: 19.0.17
|
||||
title: ""
|
||||
date: 2024-10-08T14:04:42.011Z
|
||||
- commits:
|
||||
- subject: "compose: Reduce the properties updated to only the necessary"
|
||||
hash: 7e2b5abe600ab13f5f55fc86f0850b54a66debe5
|
||||
body: ""
|
||||
footer:
|
||||
Change-type: patch
|
||||
change-type: patch
|
||||
author: Thodoris Greasidis
|
||||
nested: []
|
||||
version: 19.0.16
|
||||
title: ""
|
||||
date: 2024-10-08T13:35:01.015Z
|
||||
- commits:
|
||||
- subject: Remove unused `mockery` dev dependency
|
||||
hash: 1245b1c99bab2d504a025f489451710cc140bc55
|
||||
body: ""
|
||||
footer:
|
||||
Change-type: patch
|
||||
change-type: patch
|
||||
author: myarmolinsky
|
||||
nested: []
|
||||
version: 19.0.15
|
||||
title: ""
|
||||
date: 2024-10-08T13:11:01.319Z
|
||||
- commits:
|
||||
- subject: Temporarily skip broken image-manager tests on Windows and Mac
|
||||
hash: 77b9514442ab81ef1375a2517eb3e9aab7e724da
|
||||
body: ""
|
||||
footer:
|
||||
Change-type: patch
|
||||
change-type: patch
|
||||
author: myarmolinsky
|
||||
nested: []
|
||||
version: 19.0.14
|
||||
title: ""
|
||||
date: 2024-10-08T12:44:14.300Z
|
||||
- commits:
|
||||
- subject: Remove extra line from recent changelog entry
|
||||
hash: 127bd7ec722133d81cd94a5e028d9f2e5219df7c
|
||||
body: ""
|
||||
footer:
|
||||
Change-type: patch
|
||||
change-type: patch
|
||||
author: myarmolinsky
|
||||
nested: []
|
||||
version: 19.0.13
|
||||
title: ""
|
||||
date: 2024-09-23T11:35:45.475Z
|
||||
- commits:
|
||||
- subject: skip
|
||||
hash: c7441b06ac97a50d8ffefebff3af55e1d12d4035
|
||||
body: ""
|
||||
footer:
|
||||
Change-type: patch
|
||||
change-type: patch
|
||||
author: myarmolinsky
|
||||
nested: []
|
||||
- subject: Add `image-manager` tests
|
||||
hash: 251d64eb8831555e2cfc8a54c73701eadb8c4f06
|
||||
body: ""
|
||||
footer:
|
||||
Change-type: patch
|
||||
change-type: patch
|
||||
author: myarmolinsky
|
||||
nested: []
|
||||
- subject: Remove `balena-image-manager` dependency
|
||||
hash: ff9bb52a20b3f21281189ddfbbe1b800e104be1d
|
||||
body: ""
|
||||
footer:
|
||||
Change-type: patch
|
||||
change-type: patch
|
||||
author: myarmolinsky
|
||||
nested: []
|
||||
- subject: Embed `balena-image-manager` instead of having it as a dependency
|
||||
hash: c799c3f10d1491227fe770ceace46b26ae209b19
|
||||
body: ""
|
||||
footer:
|
||||
Change-type: patch
|
||||
change-type: patch
|
||||
author: myarmolinsky
|
||||
nested: []
|
||||
version: 19.0.12
|
||||
title: ""
|
||||
date: 2024-09-20T17:48:39.881Z
|
||||
- commits:
|
||||
- subject: Remove Bluebird as a direct dependency
|
||||
hash: d39dc5a39ad0ec25e0a690b881c8212699f64162
|
||||
body: ""
|
||||
footer:
|
||||
Change-type: patch
|
||||
change-type: patch
|
||||
author: Thodoris Greasidis
|
||||
nested: []
|
||||
version: 19.0.11
|
||||
title: ""
|
||||
date: 2024-09-18T16:38:55.929Z
|
||||
- commits:
|
||||
- subject: Remove package `@resin.io/valid-email`
|
||||
hash: a2b4f76c94e7fda3f122adf721e0a12d9a0e9164
|
||||
body: ""
|
||||
footer:
|
||||
Change-type: patch
|
||||
change-type: patch
|
||||
author: myarmolinsky
|
||||
nested: []
|
||||
version: 19.0.10
|
||||
title: ""
|
||||
date: 2024-09-12T23:00:13.119Z
|
||||
- commits:
|
||||
- subject: Update actions/download-artifact action to v4.1.8
|
||||
hash: 58f480ad7c097952b3ff4e0a5daebc163f2ce7c1
|
||||
body: |
|
||||
Update actions/download-artifact from 4.1.7 to 4.1.8
|
||||
footer:
|
||||
Change-type: patch
|
||||
change-type: patch
|
||||
author: Self-hosted Renovate Bot
|
||||
nested: []
|
||||
version: 19.0.9
|
||||
title: ""
|
||||
date: 2024-09-12T16:12:08.049Z
|
||||
- commits:
|
||||
- subject: Update actions/upload-artifact digest to 5076954
|
||||
hash: e101e0f46663585d2999c1bd59c5335a2d012ae4
|
||||
body: |
|
||||
Update actions/upload-artifact
|
||||
footer:
|
||||
Change-type: patch
|
||||
change-type: patch
|
||||
author: Self-hosted Renovate Bot
|
||||
nested: []
|
||||
version: 19.0.8
|
||||
title: ""
|
||||
date: 2024-09-12T15:07:51.366Z
|
||||
- commits:
|
||||
- subject: Update actions/setup-node digest to 1e60f62
|
||||
hash: 314e8800d0c6dfeade2140125cb3dd996713713e
|
||||
body: |
|
||||
Update actions/setup-node
|
||||
footer:
|
||||
Change-type: patch
|
||||
change-type: patch
|
||||
author: Self-hosted Renovate Bot
|
||||
nested: []
|
||||
version: 19.0.7
|
||||
title: ""
|
||||
date: 2024-09-12T14:13:18.381Z
|
||||
- commits:
|
||||
- subject: Remove moment and moment-duration-format in favor of native time parsing
|
||||
hash: 707b249e972a6943d75014f487285c0dd8085b15
|
||||
body: ""
|
||||
footer:
|
||||
Change-type: patch
|
||||
change-type: patch
|
||||
author: Otavio Jacobi
|
||||
nested: []
|
||||
version: 19.0.6
|
||||
title: ""
|
||||
date: 2024-09-12T13:47:25.357Z
|
||||
- commits:
|
||||
- subject: Update apple-actions/import-codesign-certs action to v2
|
||||
hash: 9242a3493af4c518c4d1328f19ddf2d95c182af7
|
||||
body: |
|
||||
Update apple-actions/import-codesign-certs from 1 to 2
|
||||
footer:
|
||||
Change-type: patch
|
||||
change-type: patch
|
||||
author: Self-hosted Renovate Bot
|
||||
nested: []
|
||||
version: 19.0.5
|
||||
title: ""
|
||||
date: 2024-09-10T15:13:23.938Z
|
||||
- commits:
|
||||
- subject: Update TypeScript to 5.6.2
|
||||
hash: 5f92bbc846fe93cc03ebe7717baafe24f17d4e0d
|
||||
body: ""
|
||||
footer:
|
||||
Change-type: patch
|
||||
change-type: patch
|
||||
author: Thodoris Greasidis
|
||||
nested: []
|
||||
version: 19.0.4
|
||||
title: ""
|
||||
date: 2024-09-10T14:44:39.949Z
|
||||
- commits:
|
||||
- subject: Reduce use of CJS require() on automation files
|
||||
hash: facc66e9f97d075610d4383efa92dceb5b4f7acf
|
||||
|
62
CHANGELOG.md
62
CHANGELOG.md
@ -4,6 +4,68 @@ 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/).
|
||||
|
||||
## 19.0.17 - 2024-10-08
|
||||
|
||||
* Remove dev dependency `parse-link-header` [myarmolinsky]
|
||||
* Remove dev dependency @octokit/rest [myarmolinsky]
|
||||
* Remove dev dependency @octokit/plugin-throttling [myarmolinsky]
|
||||
* Remove no longer needed references and tests for mixpanel [myarmolinsky]
|
||||
* Remove dev dependency `@types/mixpanel` [myarmolinsky]
|
||||
|
||||
## 19.0.16 - 2024-10-08
|
||||
|
||||
* compose: Reduce the properties updated to only the necessary [Thodoris Greasidis]
|
||||
|
||||
## 19.0.15 - 2024-10-08
|
||||
|
||||
* Remove unused `mockery` dev dependency [myarmolinsky]
|
||||
|
||||
## 19.0.14 - 2024-10-08
|
||||
|
||||
* Temporarily skip broken image-manager tests on Windows and Mac [myarmolinsky]
|
||||
|
||||
## 19.0.13 - 2024-09-23
|
||||
|
||||
* Remove extra line from recent changelog entry [myarmolinsky]
|
||||
|
||||
## 19.0.12 - 2024-09-20
|
||||
|
||||
* Add `image-manager` tests [myarmolinsky]
|
||||
* Remove `balena-image-manager` dependency [myarmolinsky]
|
||||
* Embed `balena-image-manager` instead of having it as a dependency [myarmolinsky]
|
||||
|
||||
## 19.0.11 - 2024-09-18
|
||||
|
||||
* Remove Bluebird as a direct dependency [Thodoris Greasidis]
|
||||
|
||||
## 19.0.10 - 2024-09-12
|
||||
|
||||
* Remove package `@resin.io/valid-email` [myarmolinsky]
|
||||
|
||||
## 19.0.9 - 2024-09-12
|
||||
|
||||
* Update actions/download-artifact action to v4.1.8 [Self-hosted Renovate Bot]
|
||||
|
||||
## 19.0.8 - 2024-09-12
|
||||
|
||||
* Update actions/upload-artifact digest to 5076954 [Self-hosted Renovate Bot]
|
||||
|
||||
## 19.0.7 - 2024-09-12
|
||||
|
||||
* Update actions/setup-node digest to 1e60f62 [Self-hosted Renovate Bot]
|
||||
|
||||
## 19.0.6 - 2024-09-12
|
||||
|
||||
* Remove moment and moment-duration-format in favor of native time parsing [Otavio Jacobi]
|
||||
|
||||
## 19.0.5 - 2024-09-10
|
||||
|
||||
* Update apple-actions/import-codesign-certs action to v2 [Self-hosted Renovate Bot]
|
||||
|
||||
## 19.0.4 - 2024-09-10
|
||||
|
||||
* Update TypeScript to 5.6.2 [Thodoris Greasidis]
|
||||
|
||||
## 19.0.3 - 2024-09-05
|
||||
|
||||
* Reduce use of CJS require() on automation files [Otavio Jacobi]
|
||||
|
@ -19,7 +19,6 @@ import type { JsonVersions } from '../src/commands/version/index';
|
||||
|
||||
import { run as oclifRun } from '@oclif/core';
|
||||
import * as archiver from 'archiver';
|
||||
import * as Bluebird from 'bluebird';
|
||||
import { exec, execFile } from 'child_process';
|
||||
import * as filehound from 'filehound';
|
||||
import type { Stats } from 'fs';
|
||||
@ -42,6 +41,7 @@ import {
|
||||
|
||||
const execFileAsync = promisify(execFile);
|
||||
const execAsync = promisify(exec);
|
||||
const rimrafAsync = promisify(rimraf);
|
||||
|
||||
export const packageJSON = loadPackageJson();
|
||||
export const version = 'v' + packageJSON.version;
|
||||
@ -517,7 +517,7 @@ export async function buildOclifInstaller() {
|
||||
}
|
||||
for (const dir of dirs) {
|
||||
console.log(`rimraf(${dir})`);
|
||||
await Bluebird.fromCallback((cb) => rimraf(dir, cb));
|
||||
await rimrafAsync(dir);
|
||||
}
|
||||
console.log('=======================================================');
|
||||
console.log(`oclif ${packCmd} ${packOpts.join(' ')}`);
|
||||
|
@ -1,215 +0,0 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2019 Balena Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import * as _ from 'lodash';
|
||||
import * as semver from 'semver';
|
||||
import { Octokit } from '@octokit/rest';
|
||||
import { throttling } from '@octokit/plugin-throttling';
|
||||
|
||||
const { GITHUB_TOKEN } = process.env;
|
||||
|
||||
/** Return a cached Octokit instance, creating a new one as needed. */
|
||||
const getOctokit = _.once(function () {
|
||||
const OctokitConstructor = Octokit.plugin(throttling);
|
||||
return new OctokitConstructor({
|
||||
auth: GITHUB_TOKEN,
|
||||
throttle: {
|
||||
onRateLimit: (retryAfter: number, options: any) => {
|
||||
console.warn(
|
||||
`Request quota exhausted for request ${options.method} ${options.url}`,
|
||||
);
|
||||
// retries 3 times
|
||||
if (options.request.retryCount < 3) {
|
||||
console.log(`Retrying after ${retryAfter} seconds!`);
|
||||
return true;
|
||||
}
|
||||
},
|
||||
onAbuseLimit: (_retryAfter: number, options: any) => {
|
||||
// does not retry, only logs a warning
|
||||
console.warn(
|
||||
`Abuse detected for request ${options.method} ${options.url}`,
|
||||
);
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Extract pagination information (current page, total pages, ordinal number)
|
||||
* from the 'link' response header (example below), using the parse-link-header
|
||||
* npm package:
|
||||
* "link": "<https://api.github.com/repositories/187370853/releases?per_page=2&page=2>; rel=\"next\",
|
||||
* <https://api.github.com/repositories/187370853/releases?per_page=2&page=3>; rel=\"last\""
|
||||
*
|
||||
* @param response Octokit response object (including response.headers.link)
|
||||
* @param perPageDefault Default per_page pagination value if missing in URL
|
||||
* @return Object where 'page' is the current page number (1-based),
|
||||
* 'pages' is the total number of pages, and 'ordinal' is the ordinal number
|
||||
* (3rd, 4th, 5th...) of the first item in the current page.
|
||||
*/
|
||||
async function getPageNumbers(
|
||||
response: any,
|
||||
perPageDefault: number,
|
||||
): Promise<{ page: number; pages: number; ordinal: number }> {
|
||||
const res = { page: 1, pages: 1, ordinal: 1 };
|
||||
if (!response.headers.link) {
|
||||
return res;
|
||||
}
|
||||
const parse = await import('parse-link-header');
|
||||
|
||||
const parsed = parse(response.headers.link);
|
||||
if (parsed == null) {
|
||||
throw new Error(`Failed to parse link header: '${response.headers.link}'`);
|
||||
}
|
||||
let perPage = perPageDefault;
|
||||
if (parsed.next) {
|
||||
if (parsed.next.per_page) {
|
||||
perPage = parseInt(parsed.next.per_page, 10);
|
||||
}
|
||||
res.page = parseInt(parsed.next.page!, 10) - 1;
|
||||
res.pages = parseInt(parsed.last!.page!, 10);
|
||||
} else {
|
||||
if (parsed.prev!.per_page) {
|
||||
perPage = parseInt(parsed.prev!.per_page, 10);
|
||||
}
|
||||
res.page = res.pages = parseInt(parsed.prev!.page!, 10) + 1;
|
||||
}
|
||||
res.ordinal = (res.page - 1) * perPage + 1;
|
||||
return res;
|
||||
}
|
||||
|
||||
/**
|
||||
* Iterate over every GitHub release in the given owner/repo, check whether
|
||||
* its tag_name matches against the affectedVersions semver spec, and if so
|
||||
* replace its release description (body) with the given newDescription value.
|
||||
* @param owner GitHub repo owner, e.g. 'balena-io' or 'pdcastro'
|
||||
* @param repo GitHub repo, e.g. 'balena-cli'
|
||||
* @param affectedVersions Semver spec, e.g. '2.6.1 - 7.10.9 || 8.0.0'
|
||||
* @param newDescription New release description (body)
|
||||
* @param editID Short string present in newDescription, e.g. '[AA101]', that
|
||||
* can be searched to determine whether that release has already been updated.
|
||||
*/
|
||||
async function updateGitHubReleaseDescriptions(
|
||||
owner: string,
|
||||
repo: string,
|
||||
affectedVersions: string,
|
||||
newDescription: string,
|
||||
editID: string,
|
||||
) {
|
||||
const perPage = 30;
|
||||
const octokit = getOctokit();
|
||||
const options = octokit.repos.listReleases.endpoint.merge({
|
||||
owner,
|
||||
repo,
|
||||
per_page: perPage,
|
||||
});
|
||||
let errCount = 0;
|
||||
type Release =
|
||||
import('@octokit/rest').RestEndpointMethodTypes['repos']['listReleases']['response']['data'][0];
|
||||
for await (const response of octokit.paginate.iterator<Release>(options)) {
|
||||
const {
|
||||
page: thisPage,
|
||||
pages: totalPages,
|
||||
ordinal,
|
||||
} = await getPageNumbers(response, perPage);
|
||||
let i = 0;
|
||||
for (const cliRelease of response.data) {
|
||||
const prefix = `[#${ordinal + i++} pg ${thisPage}/${totalPages}]`;
|
||||
if (!cliRelease.id) {
|
||||
console.error(
|
||||
`${prefix} Error: missing release ID (errCount=${++errCount})`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
const skipMsg = `${prefix} skipping release "${cliRelease.tag_name}" (${cliRelease.id})`;
|
||||
if (cliRelease.draft === true) {
|
||||
console.info(`${skipMsg}: draft release`);
|
||||
continue;
|
||||
} else if (cliRelease.body && cliRelease.body.includes(editID)) {
|
||||
console.info(`${skipMsg}: already updated`);
|
||||
continue;
|
||||
} else if (!semver.satisfies(cliRelease.tag_name, affectedVersions)) {
|
||||
console.info(`${skipMsg}: outside version range`);
|
||||
continue;
|
||||
} else {
|
||||
const updatedRelease = {
|
||||
owner,
|
||||
repo,
|
||||
release_id: cliRelease.id,
|
||||
body: newDescription,
|
||||
};
|
||||
let oldBodyPreview = cliRelease.body;
|
||||
if (oldBodyPreview) {
|
||||
oldBodyPreview = oldBodyPreview.replace(/\s+/g, ' ').trim();
|
||||
if (oldBodyPreview.length > 12) {
|
||||
oldBodyPreview = oldBodyPreview.substring(0, 9) + '...';
|
||||
}
|
||||
}
|
||||
console.info(
|
||||
`${prefix} updating release "${cliRelease.tag_name}" (${cliRelease.id}) old body="${oldBodyPreview}"`,
|
||||
);
|
||||
try {
|
||||
await octokit.repos.updateRelease(updatedRelease);
|
||||
} catch (err) {
|
||||
console.error(
|
||||
`${skipMsg}: Error: ${err.message} (count=${++errCount})`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a warning description to CLI releases affected by a mixpanel tracking
|
||||
* security issue (#1359). This function can be executed "manually" with the
|
||||
* following command line:
|
||||
*
|
||||
* npx ts-node --type-check -P automation/tsconfig.json automation/run.ts fix1359
|
||||
*/
|
||||
export async function updateDescriptionOfReleasesAffectedByIssue1359() {
|
||||
// Run only on Linux/Node10, instead of all platform/Node combinations.
|
||||
// (It could have been any other platform, as long as it only runs once.)
|
||||
if (process.platform !== 'linux' || semver.major(process.version) !== 10) {
|
||||
return;
|
||||
}
|
||||
const owner = 'balena-io';
|
||||
const repo = 'balena-cli';
|
||||
const affectedVersions =
|
||||
'2.6.1 - 7.10.9 || 8.0.0 - 8.1.0 || 9.0.0 - 9.15.6 || 10.0.0 - 10.17.5 || 11.0.0 - 11.7.2';
|
||||
const editID = '[AA100]';
|
||||
let newDescription = `
|
||||
Please note: the "login" command in this release is affected by a
|
||||
security issue fixed in versions
|
||||
[7.10.10](https://github.com/balena-io/balena-cli/releases/tag/v7.10.10),
|
||||
[8.1.1](https://github.com/balena-io/balena-cli/releases/tag/v8.1.1),
|
||||
[9.15.7](https://github.com/balena-io/balena-cli/releases/tag/v9.15.7),
|
||||
[10.17.6](https://github.com/balena-io/balena-cli/releases/tag/v10.17.6),
|
||||
[11.7.3](https://github.com/balena-io/balena-cli/releases/tag/v11.7.3)
|
||||
and later. If you need to use this version, avoid passing your password,
|
||||
keys or tokens as command-line arguments. ${editID}`;
|
||||
// remove line breaks and collapse white space
|
||||
newDescription = newDescription.replace(/\s+/g, ' ').trim();
|
||||
await updateGitHubReleaseDescriptions(
|
||||
owner,
|
||||
repo,
|
||||
affectedVersions,
|
||||
newDescription,
|
||||
editID,
|
||||
);
|
||||
}
|
@ -24,7 +24,6 @@ import {
|
||||
signFilesForNotarization,
|
||||
testShrinkwrap,
|
||||
} from './build-bin';
|
||||
import { updateDescriptionOfReleasesAffectedByIssue1359 } from './deploy-bin';
|
||||
|
||||
// DEBUG set to falsy for negative values else is truthy
|
||||
process.env.DEBUG = ['0', 'no', 'false', '', undefined].includes(
|
||||
@ -54,7 +53,6 @@ async function parse(args?: string[]) {
|
||||
'sign:binaries': signFilesForNotarization,
|
||||
'catch-uncommitted': catchUncommitted,
|
||||
'test-shrinkwrap': testShrinkwrap,
|
||||
fix1359: updateDescriptionOfReleasesAffectedByIssue1359,
|
||||
};
|
||||
for (const arg of args) {
|
||||
if (!Object.hasOwn(commands, arg)) {
|
||||
|
@ -22,7 +22,7 @@ _balena() {
|
||||
key_cmds=( add rm )
|
||||
local_cmds=( configure flash )
|
||||
os_cmds=( build-config configure download initialize versions )
|
||||
release_cmds=( export finalize import invalidate validate )
|
||||
release_cmds=( finalize invalidate validate )
|
||||
tag_cmds=( rm set )
|
||||
|
||||
|
||||
|
@ -21,7 +21,7 @@ _balena_complete()
|
||||
key_cmds="add rm"
|
||||
local_cmds="configure flash"
|
||||
os_cmds="build-config configure download initialize versions"
|
||||
release_cmds="export finalize import invalidate validate"
|
||||
release_cmds="finalize invalidate validate"
|
||||
tag_cmds="rm set"
|
||||
|
||||
|
||||
|
@ -282,9 +282,7 @@ are encouraged to regularly update the balena CLI to the latest version.
|
||||
|
||||
- Releases
|
||||
|
||||
- [release export <commitorid>](#release-export-commitorid)
|
||||
- [release finalize <commitorid>](#release-finalize-commitorid)
|
||||
- [release import <file> <fleet>](#release-import-file-fleet)
|
||||
- [release <commitorid>](#release-commitorid)
|
||||
- [release invalidate <commitorid>](#release-invalidate-commitorid)
|
||||
- [release validate <commitorid>](#release-validate-commitorid)
|
||||
@ -3347,37 +3345,6 @@ The notes for this release
|
||||
|
||||
# Releases
|
||||
|
||||
## release export <commitOrId>
|
||||
|
||||
Exporting a release to a file allows you to import an exact
|
||||
copy of the original release into another app.
|
||||
|
||||
If the SemVer of a release is provided using the --version option,
|
||||
the first argument is assumed to be the fleet's slug.
|
||||
|
||||
Only successful releases can be exported.
|
||||
|
||||
Examples:
|
||||
|
||||
$ balena release export a777f7345fe3d655c1c981aa642e5555 -o ../path/to/release.tar
|
||||
$ balena release export myOrg/myFleet --version 1.2.3 -o ../path/to/release.tar
|
||||
|
||||
### Arguments
|
||||
|
||||
#### COMMITORID
|
||||
|
||||
commit, ID, or version of the release to export
|
||||
|
||||
### Options
|
||||
|
||||
#### -o, --output OUTPUT
|
||||
|
||||
output path
|
||||
|
||||
#### --version VERSION
|
||||
|
||||
version of the release to export from the specified fleet
|
||||
|
||||
## release finalize <commitOrId>
|
||||
|
||||
Finalize a release. Releases can be "draft" or "final", and this command
|
||||
@ -3404,40 +3371,6 @@ the commit or ID of the release to finalize
|
||||
|
||||
### Options
|
||||
|
||||
## release import <file> <fleet>
|
||||
|
||||
is automatically omitted when importing a release. The backend will auto-increment
|
||||
the revision field of the imported release if a release exists with the same semver.
|
||||
A release will not be imported if a successful release with the same commit already
|
||||
exists.
|
||||
|
||||
To export a release to a file, use 'balena release export'.
|
||||
|
||||
Use the --override-version option to specify the version
|
||||
of the imported release, overriding the one saved in the file.
|
||||
|
||||
Examples:
|
||||
|
||||
$ balena release import ../path/to/release.tar myFleet
|
||||
$ balena release import ../path/to/release.tar myOrg/myFleet
|
||||
$ balena release import ../path/to/release.tar myOrg/myFleet --override-version 1.2.3
|
||||
|
||||
### Arguments
|
||||
|
||||
#### BUNDLE
|
||||
|
||||
path to a file, e.g. "./release.tar"
|
||||
|
||||
#### FLEET
|
||||
|
||||
fleet that the release will be imported to, e.g. "myOrg/myFleet"
|
||||
|
||||
### Options
|
||||
|
||||
#### --override-version OVERRIDE-VERSION
|
||||
|
||||
Imports this release with the specified version overriding the version in the file.
|
||||
|
||||
## release <commitOrId>
|
||||
|
||||
The --json option is recommended when scripting the output of this command,
|
||||
|
692
npm-shrinkwrap.json
generated
692
npm-shrinkwrap.json
generated
File diff suppressed because it is too large
Load Diff
27
package.json
27
package.json
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "balena-cli",
|
||||
"version": "19.0.3",
|
||||
"version": "19.0.17",
|
||||
"description": "The official balena Command Line Interface",
|
||||
"main": "./build/app.js",
|
||||
"homepage": "https://github.com/balena-io/balena-cli",
|
||||
@ -113,8 +113,6 @@
|
||||
"devDependencies": {
|
||||
"@balena/lint": "^8.0.0",
|
||||
"@electron/notarize": "^2.0.0",
|
||||
"@octokit/plugin-throttling": "^3.5.1",
|
||||
"@octokit/rest": "^18.6.7",
|
||||
"@types/archiver": "^6.0.2",
|
||||
"@types/bluebird": "^3.5.36",
|
||||
"@types/body-parser": "^1.19.2",
|
||||
@ -138,14 +136,13 @@
|
||||
"@types/jsonwebtoken": "^9.0.6",
|
||||
"@types/klaw": "^3.0.6",
|
||||
"@types/lodash": "^4.14.178",
|
||||
"@types/mixpanel": "^2.14.3",
|
||||
"@types/mime": "^3.0.4",
|
||||
"@types/mocha": "^10.0.7",
|
||||
"@types/mock-fs": "^4.13.4",
|
||||
"@types/mock-require": "^2.0.1",
|
||||
"@types/moment-duration-format": "^2.2.3",
|
||||
"@types/ndjson": "^2.0.1",
|
||||
"@types/node": "^20.0.0",
|
||||
"@types/node-cleanup": "^2.1.2",
|
||||
"@types/parse-link-header": "^2.0.3",
|
||||
"@types/prettyjson": "^0.0.33",
|
||||
"@types/progress-stream": "^2.0.2",
|
||||
"@types/request": "^2.48.7",
|
||||
@ -179,38 +176,34 @@
|
||||
"intercept-stdout": "^0.1.2",
|
||||
"jsonwebtoken": "^9.0.0",
|
||||
"klaw": "^4.1.0",
|
||||
"mkdirp": "^3.0.1",
|
||||
"mocha": "^10.6.0",
|
||||
"mock-fs": "^5.2.0",
|
||||
"mock-require": "^3.0.3",
|
||||
"nock": "^13.2.1",
|
||||
"oclif": "^4.14.0",
|
||||
"parse-link-header": "^2.0.0",
|
||||
"rewire": "^7.0.0",
|
||||
"simple-git": "^3.14.1",
|
||||
"sinon": "^18.0.0",
|
||||
"string-to-stream": "^3.0.1",
|
||||
"ts-node": "^10.4.0",
|
||||
"typescript": "^5.5.2"
|
||||
"typescript": "^5.6.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"@balena/compose": "^4.0.1",
|
||||
"@balena/dockerignore": "^1.0.2",
|
||||
"@balena/env-parsing": "^1.1.8",
|
||||
"@balena/es-version": "^1.0.1",
|
||||
"@balena/release-bundle": "^0.5.2",
|
||||
"@oclif/core": "^4.0.8",
|
||||
"@resin.io/valid-email": "^0.1.0",
|
||||
"@sentry/node": "^6.16.1",
|
||||
"balena-config-json": "^4.2.0",
|
||||
"balena-device-init": "^7.0.1",
|
||||
"balena-errors": "^4.7.3",
|
||||
"balena-image-fs": "^7.0.6",
|
||||
"balena-image-manager": "^10.0.1",
|
||||
"balena-preload": "^15.0.6",
|
||||
"balena-sdk": "^19.7.3",
|
||||
"balena-semver": "^2.3.0",
|
||||
"balena-settings-client": "^5.0.2",
|
||||
"balena-settings-storage": "^8.1.0",
|
||||
"bluebird": "^3.7.2",
|
||||
"body-parser": "^1.19.1",
|
||||
"bonjour-service": "^1.2.1",
|
||||
"chalk": "^3.0.0",
|
||||
@ -241,8 +234,8 @@
|
||||
"JSONStream": "^1.0.3",
|
||||
"livepush": "^3.5.1",
|
||||
"lodash": "^4.17.21",
|
||||
"moment": "^2.29.1",
|
||||
"moment-duration-format": "^2.3.2",
|
||||
"mime": "^2.4.6",
|
||||
"mkdirp": "^3.0.1",
|
||||
"ndjson": "^2.0.0",
|
||||
"node-cleanup": "^2.1.2",
|
||||
"node-unzip-2": "^0.2.8",
|
||||
@ -253,7 +246,7 @@
|
||||
"reconfix": "^1.0.0-v0-1-0-fork-46760acff4d165f5238bfac5e464256ef1944476",
|
||||
"request": "^2.88.2",
|
||||
"resin-cli-form": "^3.0.0",
|
||||
"resin-cli-visuals": "^2.0.0",
|
||||
"resin-cli-visuals": "^2.0.1",
|
||||
"resin-doodles": "^0.2.0",
|
||||
"resin-stream-logger": "^0.1.2",
|
||||
"rimraf": "^3.0.2",
|
||||
@ -281,6 +274,6 @@
|
||||
}
|
||||
},
|
||||
"versionist": {
|
||||
"publishedAt": "2024-09-05T12:34:09.871Z"
|
||||
"publishedAt": "2024-10-08T14:04:42.930Z"
|
||||
}
|
||||
}
|
||||
|
@ -89,7 +89,7 @@ export default class OsDownloadCmd extends Command {
|
||||
|
||||
// balenaOS ESR versions require user authentication
|
||||
if (options.version) {
|
||||
const { isESR } = await import('balena-image-manager');
|
||||
const { isESR } = await import('../../utils/image-manager');
|
||||
if (options.version === 'menu-esr' || isESR(options.version)) {
|
||||
try {
|
||||
await OsDownloadCmd.checkLoggedIn();
|
||||
|
@ -1,116 +0,0 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2016-2024 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 { commitOrIdArg } from '.';
|
||||
import { Flags } from '@oclif/core';
|
||||
import Command from '../../command';
|
||||
import * as cf from '../../utils/common-flags';
|
||||
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
|
||||
import { create } from '@balena/release-bundle';
|
||||
import * as fs from 'fs/promises';
|
||||
import * as semver from 'balena-semver';
|
||||
import { ExpectedError } from '../../errors';
|
||||
|
||||
export default class ReleaseExportCmd extends Command {
|
||||
public static description = stripIndent`
|
||||
Exports a release into a file.
|
||||
|
||||
Exporting a release to a file allows you to import an exact
|
||||
copy of the original release into another app.
|
||||
|
||||
If the SemVer of a release is provided using the --version option,
|
||||
the first argument is assumed to be the fleet's slug.
|
||||
|
||||
Only successful releases can be exported.
|
||||
`;
|
||||
public static examples = [
|
||||
'$ balena release export a777f7345fe3d655c1c981aa642e5555 -o ../path/to/release.tar',
|
||||
'$ balena release export myOrg/myFleet --version 1.2.3 -o ../path/to/release.tar',
|
||||
];
|
||||
|
||||
public static usage = 'release export <commitOrId>';
|
||||
|
||||
public static flags = {
|
||||
output: Flags.string({
|
||||
description: 'output path',
|
||||
char: 'o',
|
||||
required: true,
|
||||
}),
|
||||
version: Flags.string({
|
||||
description: 'version of the release to export from the specified fleet',
|
||||
}),
|
||||
help: cf.help,
|
||||
};
|
||||
|
||||
public static args = {
|
||||
commitOrId: commitOrIdArg({
|
||||
description: 'commit, ID, or version of the release to export',
|
||||
required: true,
|
||||
}),
|
||||
};
|
||||
|
||||
public static authenticated = true;
|
||||
|
||||
public async run() {
|
||||
const { args: params, flags: options } = await this.parse(ReleaseExportCmd);
|
||||
|
||||
const balena = getBalenaSdk();
|
||||
|
||||
let release: balenaSdk.Release;
|
||||
if (typeof options.version === 'string') {
|
||||
const application = params.commitOrId;
|
||||
const parsedVersion = semver.parse(options.version);
|
||||
if (parsedVersion == null) {
|
||||
throw new ExpectedError(
|
||||
`Release of ${application} with version ${options.version} could not be exported; version must be valid SemVer.`,
|
||||
);
|
||||
} else {
|
||||
const rawVersion =
|
||||
parsedVersion.build.length === 0
|
||||
? parsedVersion.version
|
||||
: `${parsedVersion.version}+${parsedVersion.build[0]}`;
|
||||
release = await balena.models.release.get(
|
||||
{ application, rawVersion },
|
||||
{ $select: ['id'] },
|
||||
);
|
||||
}
|
||||
} else {
|
||||
release = await balena.models.release.get(params.commitOrId, {
|
||||
$select: ['id'],
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
const releaseBundle = await create({
|
||||
sdk: balena,
|
||||
releaseId: release.id,
|
||||
});
|
||||
await fs.writeFile(options.output, releaseBundle);
|
||||
const versionInfo =
|
||||
typeof options.version === 'string'
|
||||
? ` version ${options.version}`
|
||||
: '';
|
||||
console.log(
|
||||
`Release ${params.commitOrId}${versionInfo} has been exported to ${options.output}.`,
|
||||
);
|
||||
} catch (error) {
|
||||
throw new ExpectedError(
|
||||
`Release ${params.commitOrId} could not be exported: ${error.message}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,103 +0,0 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2016-2024 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, Args } from '@oclif/core';
|
||||
import Command from '../../command';
|
||||
import * as cf from '../../utils/common-flags';
|
||||
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
|
||||
import { apply } from '@balena/release-bundle';
|
||||
import { createReadStream } from 'fs';
|
||||
import { ExpectedError } from '../../errors';
|
||||
|
||||
export default class ReleaseImportCmd extends Command {
|
||||
public static description = stripIndent`
|
||||
Imports a release from a file to an app or fleet. The revision field of the release
|
||||
is automatically omitted when importing a release. The backend will auto-increment
|
||||
the revision field of the imported release if a release exists with the same semver.
|
||||
A release will not be imported if a successful release with the same commit already
|
||||
exists.
|
||||
|
||||
To export a release to a file, use 'balena release export'.
|
||||
|
||||
Use the --override-version option to specify the version
|
||||
of the imported release, overriding the one saved in the file.
|
||||
`;
|
||||
public static examples = [
|
||||
'$ balena release import ../path/to/release.tar myFleet',
|
||||
'$ balena release import ../path/to/release.tar myOrg/myFleet',
|
||||
'$ balena release import ../path/to/release.tar myOrg/myFleet --override-version 1.2.3',
|
||||
];
|
||||
|
||||
public static usage = 'release import <file> <fleet>';
|
||||
|
||||
public static flags = {
|
||||
'override-version': Flags.string({
|
||||
description:
|
||||
'Imports this release with the specified version overriding the version in the file.',
|
||||
required: false,
|
||||
}),
|
||||
help: cf.help,
|
||||
};
|
||||
|
||||
public static args = {
|
||||
bundle: Args.string({
|
||||
required: true,
|
||||
description: 'path to a file, e.g. "./release.tar"',
|
||||
}),
|
||||
fleet: Args.string({
|
||||
required: true,
|
||||
description:
|
||||
'fleet that the release will be imported to, e.g. "myOrg/myFleet"',
|
||||
}),
|
||||
};
|
||||
|
||||
public static authenticated = true;
|
||||
|
||||
public async run() {
|
||||
const { args: params, flags: options } = await this.parse(ReleaseImportCmd);
|
||||
|
||||
const balena = getBalenaSdk();
|
||||
|
||||
const bundle = createReadStream(params.bundle).on('error', () => {
|
||||
throw new ExpectedError(
|
||||
`Release bundle ${params.bundle} does not exist or is not accessible.`,
|
||||
);
|
||||
});
|
||||
|
||||
try {
|
||||
const application = await balena.models.application.get(params.fleet, {
|
||||
$select: ['id'],
|
||||
});
|
||||
if (application == null) {
|
||||
throw new ExpectedError(`Fleet ${params.fleet} not found.`);
|
||||
}
|
||||
await apply({
|
||||
sdk: balena,
|
||||
application: application.id,
|
||||
stream: bundle,
|
||||
version: options['override-version'],
|
||||
});
|
||||
console.log(
|
||||
`Release bundle ${params.bundle} has been imported to ${params.fleet}.`,
|
||||
);
|
||||
} catch (error) {
|
||||
throw new ExpectedError(
|
||||
`Could not import release bundle ${params.bundle} to fleet ${params.fleet}: ${error.message}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
@ -59,7 +59,6 @@ export async function trackCommand(commandSignature: string) {
|
||||
});
|
||||
});
|
||||
}
|
||||
// Don't actually call mixpanel.track() while running test cases, or if suppressed
|
||||
if (
|
||||
!process.env.BALENA_CLI_TEST_TYPE &&
|
||||
!process.env.BALENARC_NO_ANALYTICS
|
||||
@ -75,9 +74,6 @@ export async function trackCommand(commandSignature: string) {
|
||||
|
||||
const TIMEOUT = 4000;
|
||||
|
||||
/**
|
||||
* 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 = {
|
||||
|
@ -145,8 +145,8 @@ export async function downloadOSImage(
|
||||
// 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);
|
||||
const { getStream } = await import('./image-manager');
|
||||
const stream = await getStream(deviceType, OSVersion);
|
||||
|
||||
const displayVersion = await new Promise((resolve, reject) => {
|
||||
stream.on('error', reject);
|
||||
|
@ -308,6 +308,21 @@ export const authorizePush = function (
|
||||
|
||||
// utilities
|
||||
|
||||
const formatDuration = (seconds: number): string => {
|
||||
const SECONDS_PER_MINUTE = 60;
|
||||
const SECONDS_PER_HOUR = 3600;
|
||||
|
||||
const hours = Math.floor(seconds / SECONDS_PER_HOUR);
|
||||
seconds %= SECONDS_PER_HOUR;
|
||||
|
||||
const minutes = Math.floor(seconds / SECONDS_PER_MINUTE);
|
||||
seconds = Math.floor(seconds % SECONDS_PER_MINUTE);
|
||||
|
||||
return hours > 0
|
||||
? `${hours}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`
|
||||
: `${minutes}:${seconds.toString().padStart(2, '0')}`;
|
||||
};
|
||||
|
||||
const renderProgressBar = function (percentage: number, stepCount: number) {
|
||||
const _ = require('lodash') as typeof import('lodash');
|
||||
percentage = _.clamp(percentage, 0, 100);
|
||||
@ -479,11 +494,6 @@ export class BuildProgressUI implements Renderer {
|
||||
}
|
||||
|
||||
_renderStatus(end = false) {
|
||||
const moment = require('moment') as typeof import('moment');
|
||||
(
|
||||
require('moment-duration-format') as typeof import('moment-duration-format')
|
||||
)(moment);
|
||||
|
||||
this._tty.clearLine();
|
||||
this._tty.write(this._prefix);
|
||||
if (end && this._cancelled) {
|
||||
@ -495,12 +505,8 @@ export class BuildProgressUI implements Renderer {
|
||||
const durationStr =
|
||||
this._startTime == null
|
||||
? 'unknown time'
|
||||
: moment
|
||||
.duration(
|
||||
Math.floor((Date.now() - this._startTime) / 1000),
|
||||
'seconds',
|
||||
)
|
||||
.format();
|
||||
: formatDuration((Date.now() - this._startTime) / 1000);
|
||||
|
||||
this._tty.writeLine(`Built ${serviceStr} in ${durationStr}`);
|
||||
} else {
|
||||
this._tty.writeLine(`Building services... ${this._spinner()}`);
|
||||
@ -577,11 +583,6 @@ export class BuildProgressInline implements Renderer {
|
||||
}
|
||||
|
||||
end(summary?: Dictionary<string>) {
|
||||
const moment = require('moment') as typeof import('moment');
|
||||
(
|
||||
require('moment-duration-format') as typeof import('moment-duration-format')
|
||||
)(moment);
|
||||
|
||||
if (this._ended) {
|
||||
return;
|
||||
}
|
||||
@ -599,12 +600,7 @@ export class BuildProgressInline implements Renderer {
|
||||
const durationStr =
|
||||
this._startTime == null
|
||||
? 'unknown time'
|
||||
: moment
|
||||
.duration(
|
||||
Math.floor((Date.now() - this._startTime) / 1000),
|
||||
'seconds',
|
||||
)
|
||||
.format();
|
||||
: formatDuration((Date.now() - this._startTime) / 1000);
|
||||
this._outStream.write(`Built ${serviceStr} in ${durationStr}\n`);
|
||||
}
|
||||
|
||||
|
@ -1343,7 +1343,24 @@ async function pushServiceImages(
|
||||
if (skipLogUpload) {
|
||||
delete serviceImage.build_log;
|
||||
}
|
||||
await releaseMod.updateImage(pineClient, serviceImage.id, serviceImage);
|
||||
|
||||
await releaseMod.updateImage(
|
||||
pineClient,
|
||||
serviceImage.id,
|
||||
// These are the only update-able image fields in bC atm, and passing
|
||||
// the whole image object in v7+ would result the allowlist to reject the request.
|
||||
_.pick(serviceImage, [
|
||||
'end_timestamp',
|
||||
'project_type',
|
||||
'error_message',
|
||||
'build_log',
|
||||
'push_timestamp',
|
||||
'status',
|
||||
'content_hash',
|
||||
'dockerfile',
|
||||
'image_size',
|
||||
]),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
@ -1429,7 +1446,10 @@ export async function deployProject(
|
||||
await runSpinner(tty, spinner, `${prefix}Saving release...`, async () => {
|
||||
release.end_timestamp = new Date();
|
||||
if (release.id != null) {
|
||||
await releaseMod.updateRelease(pineClient, release.id, release);
|
||||
await releaseMod.updateRelease(pineClient, release.id, {
|
||||
status: release.status,
|
||||
end_timestamp: release.end_timestamp,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -309,7 +309,6 @@ function connectToDocker(host: string, port: number): Docker {
|
||||
return new Docker({
|
||||
host,
|
||||
port,
|
||||
Promise: require('bluebird'),
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -410,7 +410,7 @@ export function getProxyConfig(): ProxyConfig | undefined {
|
||||
const proxyUrl = process.env.HTTPS_PROXY || process.env.HTTP_PROXY;
|
||||
if (proxyUrl) {
|
||||
const { URL } = require('url') as typeof import('url');
|
||||
let url: URL;
|
||||
let url: InstanceType<typeof URL>;
|
||||
try {
|
||||
url = new URL(proxyUrl);
|
||||
} catch (_e) {
|
||||
|
299
src/utils/image-manager.ts
Normal file
299
src/utils/image-manager.ts
Normal file
@ -0,0 +1,299 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2019 Balena Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type * as SDK from 'balena-sdk';
|
||||
import { getBalenaSdk } from './lazy';
|
||||
|
||||
// eslint-disable-next-line no-useless-escape
|
||||
const BALENAOS_VERSION_REGEX = /v?\d+\.\d+\.\d+(\.rev\d+)?((\-|\+).+)?/;
|
||||
|
||||
/**
|
||||
* @summary Check if the string is a valid balenaOS version number
|
||||
* @description Throws an error if the version is invalid
|
||||
*
|
||||
* @param {String} version - version number to validate
|
||||
* @returns {void} the most recent compatible version.
|
||||
*/
|
||||
const validateVersion = (version: string) => {
|
||||
if (!BALENAOS_VERSION_REGEX.test(version)) {
|
||||
throw new Error('Invalid version number');
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @summary Get file created date
|
||||
*
|
||||
* @param {String} filePath - file path
|
||||
* @returns {Promise<Date>} date since creation
|
||||
*
|
||||
* @example
|
||||
* getFileCreatedDate('foo/bar').then (createdTime) ->
|
||||
* console.log("The file was created in #{createdTime}")
|
||||
*/
|
||||
export const getFileCreatedDate = async (filePath: string) => {
|
||||
const { promises: fs } = await import('fs');
|
||||
const { ctime } = await fs.stat(filePath);
|
||||
return ctime;
|
||||
};
|
||||
|
||||
/**
|
||||
* @summary Get path to image in cache
|
||||
*
|
||||
* @param {String} deviceType - device type slug or alias
|
||||
* @param {String} version - the exact balenaOS version number
|
||||
* @returns {Promise<String>} image path
|
||||
*
|
||||
* @example
|
||||
* getImagePath('raspberry-pi', '1.2.3').then (imagePath) ->
|
||||
* console.log(imagePath)
|
||||
*/
|
||||
export const getImagePath = async (deviceType: string, version?: string) => {
|
||||
if (typeof version === 'string') {
|
||||
validateVersion(version);
|
||||
}
|
||||
const balena = getBalenaSdk();
|
||||
const [cacheDirectory, deviceTypeInfo] = await Promise.all([
|
||||
balena.settings.get('cacheDirectory'),
|
||||
balena.models.config.getDeviceTypeManifestBySlug(deviceType),
|
||||
]);
|
||||
const extension = deviceTypeInfo.yocto.fstype === 'zip' ? 'zip' : 'img';
|
||||
const path = await import('path');
|
||||
return path.join(cacheDirectory, `${deviceType}-v${version}.${extension}`);
|
||||
};
|
||||
|
||||
/**
|
||||
* @summary Determine if a device image is fresh
|
||||
*
|
||||
* @description
|
||||
* If the device image does not exist, return false.
|
||||
*
|
||||
* @param {String} deviceType - device type slug or alias
|
||||
* @param {String} version - the exact balenaOS version number
|
||||
* @returns {Promise<Boolean>} is image fresh
|
||||
*
|
||||
* @example
|
||||
* isImageFresh('raspberry-pi', '1.2.3').then (isFresh) ->
|
||||
* if isFresh
|
||||
* console.log('The Raspberry Pi image v1.2.3 is fresh!')
|
||||
*/
|
||||
export const isImageFresh = async (deviceType: string, version: string) => {
|
||||
const imagePath = await getImagePath(deviceType, version);
|
||||
let createdDate;
|
||||
try {
|
||||
createdDate = await getFileCreatedDate(imagePath);
|
||||
} catch {
|
||||
// Swallow errors from getFileCreatedTime.
|
||||
}
|
||||
if (createdDate == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const balena = getBalenaSdk();
|
||||
const lastModifiedDate = await balena.models.os.getLastModified(
|
||||
deviceType,
|
||||
version,
|
||||
);
|
||||
return lastModifiedDate < createdDate;
|
||||
};
|
||||
|
||||
/**
|
||||
* Heuristically determine whether the given semver version is a balenaOS
|
||||
* ESR version.
|
||||
*
|
||||
* @param {string} version Semver version. If invalid or range, return false.
|
||||
*/
|
||||
export const isESR = (version: string) => {
|
||||
const match = version.match(/^v?(\d+)\.\d+\.\d+/);
|
||||
const major = parseInt((match && match[1]) || '', 10);
|
||||
return major >= 2018; // note: (NaN >= 2018) is false
|
||||
};
|
||||
|
||||
/**
|
||||
* @summary Get the most recent compatible version
|
||||
*
|
||||
* @param {String} deviceType - device type slug or alias
|
||||
* @param {String} versionOrRange - supports the same version options
|
||||
* as `balena.models.os.getMaxSatisfyingVersion`.
|
||||
* See `getStream` for the detailed explanation.
|
||||
* @returns {Promise<String>} the most recent compatible version.
|
||||
*/
|
||||
const resolveVersion = async (deviceType: string, versionOrRange: string) => {
|
||||
const balena = getBalenaSdk();
|
||||
const version = await balena.models.os.getMaxSatisfyingVersion(
|
||||
deviceType,
|
||||
versionOrRange,
|
||||
isESR(versionOrRange) ? 'esr' : 'default',
|
||||
);
|
||||
if (!version) {
|
||||
throw new Error('No such version for the device type');
|
||||
}
|
||||
return version;
|
||||
};
|
||||
|
||||
/**
|
||||
* @summary Get an image from the cache
|
||||
*
|
||||
* @param {String} deviceType - device type slug or alias
|
||||
* @param {String} version - the exact balenaOS version number
|
||||
* @returns {Promise<fs.ReadStream>} image readable stream
|
||||
*
|
||||
* @example
|
||||
* getImage('raspberry-pi', '1.2.3').then (stream) ->
|
||||
* stream.pipe(fs.createWriteStream('foo/bar.img'))
|
||||
*/
|
||||
export const getImage = async (deviceType: string, version: string) => {
|
||||
const imagePath = await getImagePath(deviceType, version);
|
||||
const fs = await import('fs');
|
||||
const stream = fs.createReadStream(imagePath) as ReturnType<
|
||||
typeof fs.createReadStream
|
||||
> & { mime: string };
|
||||
// Default to application/octet-stream if we could not find a more specific mime type
|
||||
|
||||
const { getType } = await import('mime');
|
||||
stream.mime = getType(imagePath) ?? 'application/octet-stream';
|
||||
return stream;
|
||||
};
|
||||
|
||||
/**
|
||||
* @summary Get a writable stream for an image in the cache
|
||||
*
|
||||
* @param {String} deviceType - device type slug or alias
|
||||
* @param {String} version - the exact balenaOS version number
|
||||
* @returns {Promise<fs.WriteStream & { persistCache: () => Promise<void>, removeCache: () => Promise<void> }>} image writable stream
|
||||
*
|
||||
* @example
|
||||
* getImageWritableStream('raspberry-pi', '1.2.3').then (stream) ->
|
||||
* fs.createReadStream('foo/bar').pipe(stream)
|
||||
*/
|
||||
export const getImageWritableStream = async (
|
||||
deviceType: string,
|
||||
version?: string,
|
||||
) => {
|
||||
const imagePath = await getImagePath(deviceType, version);
|
||||
|
||||
// Ensure the cache directory exists, to prevent
|
||||
// ENOENT errors when trying to write to it.
|
||||
const path = await import('path');
|
||||
const { mkdirp } = await import('mkdirp');
|
||||
await mkdirp(path.dirname(imagePath));
|
||||
|
||||
// Append .inprogress to streams, move them to the right location only on success
|
||||
const inProgressPath = imagePath + '.inprogress';
|
||||
const { promises, createWriteStream } = await import('fs');
|
||||
type ImageWritableStream = ReturnType<typeof createWriteStream> &
|
||||
Record<'persistCache' | 'removeCache', () => Promise<void>>;
|
||||
const stream = createWriteStream(inProgressPath) as ImageWritableStream;
|
||||
|
||||
// Call .isCompleted on the stream
|
||||
stream.persistCache = () => promises.rename(inProgressPath, imagePath);
|
||||
|
||||
stream.removeCache = () => promises.unlink(inProgressPath);
|
||||
|
||||
return stream;
|
||||
};
|
||||
|
||||
type DownloadConfig = NonNullable<
|
||||
Parameters<SDK.BalenaSDK['models']['os']['download']>[0]
|
||||
>;
|
||||
|
||||
const doDownload = async (options: DownloadConfig) => {
|
||||
const balena = getBalenaSdk();
|
||||
const imageStream = await balena.models.os.download(options);
|
||||
// Piping to a PassThrough stream is needed to be able
|
||||
// to then pipe the stream to multiple destinations.
|
||||
const { PassThrough } = await import('stream');
|
||||
const pass = new PassThrough();
|
||||
imageStream.pipe(pass);
|
||||
|
||||
// Save a copy of the image in the cache
|
||||
const cacheStream = await getImageWritableStream(
|
||||
options.deviceType,
|
||||
options.version,
|
||||
);
|
||||
|
||||
pass.pipe(cacheStream, { end: false });
|
||||
pass.on('end', cacheStream.persistCache);
|
||||
|
||||
// If we return `pass` directly, the client will not be able
|
||||
// to read all data from it after a delay, since it will be
|
||||
// instantly piped to `cacheStream`.
|
||||
// The solution is to create yet another PassThrough stream,
|
||||
// pipe to it and return the new stream instead.
|
||||
const pass2 = new PassThrough() as InstanceType<typeof PassThrough> & {
|
||||
mime: string;
|
||||
};
|
||||
pass2.mime = imageStream.mime;
|
||||
imageStream.on('progress', (state) => pass2.emit('progress', state));
|
||||
|
||||
imageStream.on('error', async (err) => {
|
||||
await cacheStream.removeCache();
|
||||
pass2.emit('error', err);
|
||||
});
|
||||
|
||||
return pass.pipe(pass2);
|
||||
};
|
||||
|
||||
/**
|
||||
* @summary Get a device operating system image
|
||||
* @public
|
||||
*
|
||||
* @description
|
||||
* This function saves a copy of the downloaded image in the cache directory setting specified in [balena-settings-client](https://github.com/balena-io-modules/balena-settings-client).
|
||||
*
|
||||
* @param {String} deviceType - device type slug or alias
|
||||
* @param {String} versionOrRange - can be one of
|
||||
* * the exact version number,
|
||||
* in which case it is used if the version is supported,
|
||||
* or the promise is rejected,
|
||||
* * a [semver](https://www.npmjs.com/package/semver)-compatible
|
||||
* range specification, in which case the most recent satisfying version is used
|
||||
* if it exists, or the promise is rejected,
|
||||
* * `'latest'` in which case the most recent version is used, including pre-releases,
|
||||
* * `'recommended'` in which case the recommended version is used, i.e. the most
|
||||
* recent version excluding pre-releases, the promise is rejected
|
||||
* if only pre-release versions are available,
|
||||
* * `'default'` in which case the recommended version is used if available,
|
||||
* or `latest` is used otherwise.
|
||||
* Defaults to `'latest'`.
|
||||
* @param {Object} options
|
||||
* @param {boolean} options?.developmentMode
|
||||
* @returns {Promise<NodeJS.ReadableStream>} image readable stream
|
||||
*
|
||||
* @example
|
||||
* getStream('raspberry-pi', 'default').then (stream) ->
|
||||
* stream.pipe(fs.createWriteStream('foo/bar.img'))
|
||||
*/
|
||||
export const getStream = async (
|
||||
deviceType: string,
|
||||
versionOrRange?: string,
|
||||
options: Omit<DownloadConfig, 'deviceType' | 'version'> = {},
|
||||
) => {
|
||||
if (versionOrRange == null) {
|
||||
versionOrRange = 'latest';
|
||||
}
|
||||
const version = await resolveVersion(deviceType, versionOrRange);
|
||||
const isFresh = await isImageFresh(deviceType, version);
|
||||
const $stream = isFresh
|
||||
? await getImage(deviceType, version)
|
||||
: await doDownload({ ...options, deviceType, version });
|
||||
// schedule the 'version' event for the next iteration of the event loop
|
||||
// so that callers have a chance of adding an event handler
|
||||
setImmediate(() =>
|
||||
$stream.emit('balena-image-manager:resolved-version', version),
|
||||
);
|
||||
return $stream;
|
||||
};
|
@ -13,10 +13,13 @@ 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 validEmail = require('@resin.io/valid-email');
|
||||
import { ExpectedError } from '../errors';
|
||||
|
||||
// Sufficiently good email regex in order not to bring in another dependency.
|
||||
const isValidEmail = (email: string): boolean => {
|
||||
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
|
||||
};
|
||||
|
||||
const APPNAME_REGEX = new RegExp(/^[a-zA-Z0-9_-]+$/);
|
||||
// An regex to detect an IP address, from https://www.regular-expressions.info/ip.html
|
||||
const IP_REGEX = new RegExp(
|
||||
@ -26,7 +29,7 @@ const DOTLOCAL_REGEX = new RegExp(/^([a-zA-Z0-9-]+\.)+local$/);
|
||||
const UUID_REGEX = new RegExp(/^[0-9a-f]+$/);
|
||||
|
||||
export function validateEmail(input: string) {
|
||||
if (!validEmail(input)) {
|
||||
if (!isValidEmail(input)) {
|
||||
return 'Email is not valid';
|
||||
}
|
||||
|
||||
|
@ -1,4 +1,3 @@
|
||||
import * as Bluebird from 'bluebird';
|
||||
import { expect } from 'chai';
|
||||
import * as sinon from 'sinon';
|
||||
import * as url from 'url';
|
||||
@ -19,28 +18,28 @@ describe('Utils:', async function () {
|
||||
));
|
||||
|
||||
it('should eventually contain an https protocol', () =>
|
||||
Bluebird.props({
|
||||
dashboardUrl: balena.settings.get('dashboardUrl'),
|
||||
loginUrl: utils.getDashboardLoginURL('https://127.0.0.1:3000/callback'),
|
||||
}).then(function ({ dashboardUrl, loginUrl }) {
|
||||
Promise.all([
|
||||
balena.settings.get('dashboardUrl'),
|
||||
utils.getDashboardLoginURL('https://127.0.0.1:3000/callback'),
|
||||
]).then(function ([dashboardUrl, loginUrl]) {
|
||||
const { protocol } = url.parse(loginUrl);
|
||||
return expect(protocol).to.equal(url.parse(dashboardUrl).protocol);
|
||||
}));
|
||||
|
||||
it('should correctly escape a callback url without a path', () =>
|
||||
Bluebird.props({
|
||||
dashboardUrl: balena.settings.get('dashboardUrl'),
|
||||
loginUrl: utils.getDashboardLoginURL('http://127.0.0.1:3000'),
|
||||
}).then(function ({ dashboardUrl, loginUrl }) {
|
||||
Promise.all([
|
||||
balena.settings.get('dashboardUrl'),
|
||||
utils.getDashboardLoginURL('http://127.0.0.1:3000'),
|
||||
]).then(function ([dashboardUrl, loginUrl]) {
|
||||
const expectedUrl = `${dashboardUrl}/login/cli/http%253A%252F%252F127.0.0.1%253A3000`;
|
||||
return expect(loginUrl).to.equal(expectedUrl);
|
||||
}));
|
||||
|
||||
return it('should correctly escape a callback url with a path', () =>
|
||||
Bluebird.props({
|
||||
dashboardUrl: balena.settings.get('dashboardUrl'),
|
||||
loginUrl: utils.getDashboardLoginURL('http://127.0.0.1:3000/callback'),
|
||||
}).then(function ({ dashboardUrl, loginUrl }) {
|
||||
Promise.all([
|
||||
balena.settings.get('dashboardUrl'),
|
||||
utils.getDashboardLoginURL('http://127.0.0.1:3000/callback'),
|
||||
]).then(function ([dashboardUrl, loginUrl]) {
|
||||
const expectedUrl = `${dashboardUrl}/login/cli/http%253A%252F%252F127.0.0.1%253A3000%252Fcallback`;
|
||||
return expect(loginUrl).to.equal(expectedUrl);
|
||||
}));
|
||||
|
@ -36,7 +36,6 @@ describe('balena app create', function () {
|
||||
// Temporarily skipped because of parse/checking order issue with -h
|
||||
it.skip('should print help text with the -h flag', async () => {
|
||||
api.expectGetWhoAmI({ optional: true });
|
||||
api.expectGetMixpanel({ optional: true });
|
||||
|
||||
const { out, err } = await runCommand('app create -h');
|
||||
|
||||
|
@ -91,7 +91,6 @@ describe('balena build', function () {
|
||||
api = new BalenaAPIMock();
|
||||
docker = new DockerMock();
|
||||
api.expectGetWhoAmI({ optional: true, persist: true });
|
||||
api.expectGetMixpanel({ optional: true });
|
||||
docker.expectGetPing();
|
||||
docker.expectGetVersion({ persist: true });
|
||||
});
|
||||
@ -619,7 +618,6 @@ describe('balena build: project validation', function () {
|
||||
|
||||
this.beforeEach(() => {
|
||||
api = new BalenaAPIMock();
|
||||
api.expectGetMixpanel({ optional: true });
|
||||
});
|
||||
|
||||
this.afterEach(() => {
|
||||
|
@ -82,7 +82,6 @@ describe('balena deploy', function () {
|
||||
api = new BalenaAPIMock();
|
||||
docker = new DockerMock();
|
||||
api.expectGetWhoAmI({ optional: true, persist: true });
|
||||
api.expectGetMixpanel({ optional: true });
|
||||
api.expectGetApplication({ expandArchitecture: true });
|
||||
api.expectGetRelease();
|
||||
api.expectGetUser();
|
||||
@ -515,7 +514,6 @@ describe('balena deploy: project validation', function () {
|
||||
this.beforeEach(() => {
|
||||
api = new BalenaAPIMock();
|
||||
api.expectGetWhoAmI({ optional: true, persist: true });
|
||||
api.expectGetMixpanel({ optional: true });
|
||||
});
|
||||
|
||||
this.afterEach(() => {
|
||||
|
@ -25,7 +25,6 @@ describe('balena device move', function () {
|
||||
beforeEach(() => {
|
||||
api = new BalenaAPIMock();
|
||||
api.expectGetWhoAmI({ optional: true, persist: true });
|
||||
api.expectGetMixpanel({ optional: true });
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
|
@ -27,7 +27,6 @@ describe('balena device', function () {
|
||||
beforeEach(() => {
|
||||
api = new BalenaAPIMock();
|
||||
api.expectGetWhoAmI({ optional: true, persist: true });
|
||||
api.expectGetMixpanel({ optional: true });
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
|
@ -27,7 +27,6 @@ describe('balena devices', function () {
|
||||
beforeEach(() => {
|
||||
api = new BalenaAPIMock();
|
||||
api.expectGetWhoAmI({ optional: true, persist: true });
|
||||
api.expectGetMixpanel({ optional: true });
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
|
@ -26,7 +26,6 @@ describe('balena devices supported', function () {
|
||||
beforeEach(() => {
|
||||
api = new BalenaAPIMock();
|
||||
api.expectGetWhoAmI({ optional: true });
|
||||
api.expectGetMixpanel({ optional: true });
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
|
1
tests/commands/env/add.spec.ts
vendored
1
tests/commands/env/add.spec.ts
vendored
@ -28,7 +28,6 @@ describe('balena env add', function () {
|
||||
beforeEach(() => {
|
||||
api = new BalenaAPIMock();
|
||||
api.expectGetWhoAmI({ optional: true, persist: true });
|
||||
api.expectGetMixpanel({ optional: true });
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
|
1
tests/commands/env/envs.spec.ts
vendored
1
tests/commands/env/envs.spec.ts
vendored
@ -31,7 +31,6 @@ describe('balena envs', function () {
|
||||
beforeEach(() => {
|
||||
api = new BalenaAPIMock();
|
||||
api.expectGetWhoAmI({ optional: true, persist: true });
|
||||
api.expectGetMixpanel({ optional: true });
|
||||
// Random device UUID used to frustrate _.memoize() in utils/cloud.ts
|
||||
fullUUID = randomBytes(16).toString('hex');
|
||||
shortUUID = fullUUID.substring(0, 7);
|
||||
|
1
tests/commands/env/rename.spec.ts
vendored
1
tests/commands/env/rename.spec.ts
vendored
@ -26,7 +26,6 @@ describe('balena env rename', function () {
|
||||
beforeEach(() => {
|
||||
api = new BalenaAPIMock();
|
||||
api.expectGetWhoAmI({ optional: true, persist: true });
|
||||
api.expectGetMixpanel({ optional: true });
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
|
1
tests/commands/env/rm.spec.ts
vendored
1
tests/commands/env/rm.spec.ts
vendored
@ -26,7 +26,6 @@ describe('balena env rm', function () {
|
||||
beforeEach(() => {
|
||||
api = new BalenaAPIMock();
|
||||
api.expectGetWhoAmI({ optional: true, persist: true });
|
||||
api.expectGetMixpanel({ optional: true });
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
|
@ -111,7 +111,6 @@ describe.skip('balena help', function () {
|
||||
|
||||
this.beforeEach(() => {
|
||||
api = new BalenaAPIMock();
|
||||
api.expectGetMixpanel({ optional: true });
|
||||
});
|
||||
|
||||
this.afterEach(() => {
|
||||
|
@ -31,7 +31,6 @@ describe('balena logs', function () {
|
||||
api = new BalenaAPIMock();
|
||||
supervisor = new SupervisorMock();
|
||||
api.expectGetWhoAmI();
|
||||
api.expectGetMixpanel({ optional: true });
|
||||
});
|
||||
|
||||
this.afterEach(() => {
|
||||
|
@ -35,7 +35,6 @@ if (process.platform !== 'win32') {
|
||||
beforeEach(async () => {
|
||||
api = new BalenaAPIMock();
|
||||
api.expectGetWhoAmI({ optional: true, persist: true });
|
||||
api.expectGetMixpanel({ optional: true });
|
||||
tmpPath = (await tmpNameAsync()) as string;
|
||||
await fs.copyFile('./tests/test-data/dummy.img', tmpPath);
|
||||
});
|
||||
|
@ -89,7 +89,6 @@ describe('balena push', function () {
|
||||
api = new BalenaAPIMock();
|
||||
builder = new BuilderMock();
|
||||
api.expectGetWhoAmI({ optional: true, persist: true });
|
||||
api.expectGetMixpanel({ optional: true });
|
||||
api.expectGetApplication();
|
||||
});
|
||||
|
||||
@ -518,7 +517,6 @@ describe('balena push: project validation', function () {
|
||||
|
||||
this.beforeEach(() => {
|
||||
api = new BalenaAPIMock();
|
||||
api.expectGetMixpanel({ optional: true });
|
||||
});
|
||||
|
||||
this.afterEach(() => {
|
||||
|
@ -26,7 +26,6 @@ describe('balena release', function () {
|
||||
beforeEach(() => {
|
||||
api = new BalenaAPIMock();
|
||||
api.expectGetWhoAmI({ optional: true, persist: true });
|
||||
api.expectGetMixpanel({ optional: true });
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
|
@ -1,76 +0,0 @@
|
||||
import * as stream from 'node:stream';
|
||||
import { cleanOutput, runCommand } from '../../helpers';
|
||||
import { BalenaAPIMock } from '../../nock/balena-api-mock';
|
||||
import { expect } from 'chai';
|
||||
import * as mock from 'mock-require';
|
||||
import * as sinon from 'sinon';
|
||||
|
||||
// "itSS" means "it() Skip Standalone"
|
||||
const itSS = process.env.BALENA_CLI_TEST_TYPE === 'standalone' ? it.skip : it;
|
||||
|
||||
describe('export fleet content to a file', function () {
|
||||
let api: BalenaAPIMock;
|
||||
const releaseBundleCreateStub = sinon.stub();
|
||||
|
||||
this.beforeEach(() => {
|
||||
api = new BalenaAPIMock();
|
||||
mock('@balena/release-bundle', {
|
||||
create: releaseBundleCreateStub,
|
||||
});
|
||||
});
|
||||
|
||||
this.afterEach(() => {
|
||||
// Check all expected api calls have been made and clean up.
|
||||
api.done();
|
||||
mock.stop('@balena/release-bundle');
|
||||
});
|
||||
|
||||
itSS('should export a release to a file', async () => {
|
||||
api.expectGetWhoAmI();
|
||||
api.expectGetRelease();
|
||||
releaseBundleCreateStub.resolves(stream.Readable.from('something'));
|
||||
|
||||
const { out, err } = await runCommand(
|
||||
'release export badc0ffe -o /tmp/release.tar.gz',
|
||||
);
|
||||
|
||||
const lines = cleanOutput(out);
|
||||
expect(lines[0]).to.contain(
|
||||
'Release badc0ffe has been exported to /tmp/release.tar.gz.',
|
||||
);
|
||||
expect(err).to.be.empty;
|
||||
});
|
||||
|
||||
itSS('should fail if the create throws an error', async () => {
|
||||
api.expectGetWhoAmI();
|
||||
api.expectGetRelease();
|
||||
releaseBundleCreateStub.rejects(
|
||||
new Error('Something went wrong creating the bundle'),
|
||||
);
|
||||
|
||||
const { err } = await runCommand(
|
||||
'release export badc0ffe -o /tmp/release.tar.gz',
|
||||
);
|
||||
|
||||
expect(cleanOutput(err, true)).to.include(
|
||||
'Release badc0ffe could not be exported: Something went wrong creating the bundle',
|
||||
);
|
||||
});
|
||||
|
||||
itSS('should parse with application slug and version', async () => {
|
||||
api.expectGetWhoAmI();
|
||||
api.expectGetRelease();
|
||||
api.expectGetApplication();
|
||||
releaseBundleCreateStub.resolves(stream.Readable.from('something'));
|
||||
|
||||
const { out, err } = await runCommand(
|
||||
'release export org/superApp -o /tmp/release.tar.gz --version 1.2.3+rev1',
|
||||
);
|
||||
|
||||
const lines = cleanOutput(out);
|
||||
expect(lines[0]).to.contain(
|
||||
'Release org/superApp version 1.2.3+rev1 has been exported to /tmp/release.tar.gz.',
|
||||
);
|
||||
expect(err).to.be.empty;
|
||||
});
|
||||
});
|
@ -76,7 +76,6 @@ describe('balena ssh', function () {
|
||||
|
||||
this.beforeEach(function () {
|
||||
api = new BalenaAPIMock();
|
||||
api.expectGetMixpanel({ optional: true });
|
||||
});
|
||||
|
||||
this.afterEach(function () {
|
||||
|
@ -28,7 +28,6 @@ describe('balena tag set', function () {
|
||||
beforeEach(() => {
|
||||
api = new BalenaAPIMock();
|
||||
api.expectGetWhoAmI({ optional: true, persist: true });
|
||||
api.expectGetMixpanel({ optional: true });
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
|
@ -30,7 +30,6 @@ describe('balena version', function () {
|
||||
|
||||
this.beforeEach(() => {
|
||||
api = new BalenaAPIMock();
|
||||
api.expectGetMixpanel({ optional: true });
|
||||
});
|
||||
|
||||
this.afterEach(() => {
|
||||
|
@ -24,7 +24,6 @@ describe('balena whoami', function () {
|
||||
|
||||
this.beforeEach(() => {
|
||||
api = new BalenaAPIMock();
|
||||
api.expectGetMixpanel({ optional: true });
|
||||
});
|
||||
|
||||
this.afterEach(async () => {
|
||||
|
@ -68,7 +68,6 @@ describe('DeprecationChecker', function () {
|
||||
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);
|
||||
|
@ -74,7 +74,6 @@ export class BalenaAPIMock extends NockMock {
|
||||
"vpnEndpoint":"vpn.balena-cloud.com",
|
||||
"registryEndpoint":"registry2.balena-cloud.com",
|
||||
"deltaEndpoint":"https://delta.balena-cloud.com",
|
||||
"mixpanelToken":"",
|
||||
"apiKey":"nothingtoseehere"
|
||||
}`),
|
||||
);
|
||||
@ -465,10 +464,6 @@ export class BalenaAPIMock extends NockMock {
|
||||
public expectWhoAmIFail(opts: ScopeOpts = { optional: true }) {
|
||||
this.optGet('/actor/v1/whoami', opts).reply(401);
|
||||
}
|
||||
|
||||
public expectGetMixpanel(opts: ScopeOpts = {}) {
|
||||
this.optGet(/^\/mixpanel\/track/, opts).reply(200, {});
|
||||
}
|
||||
}
|
||||
|
||||
const appServiceVarsByService: { [key: string]: any } = {
|
||||
|
@ -15,11 +15,13 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import * as Bluebird from 'bluebird';
|
||||
import * as path from 'path';
|
||||
import * as zlib from 'zlib';
|
||||
|
||||
import { NockMock } from './nock-mock';
|
||||
import { promisify } from 'util';
|
||||
|
||||
const gunzipAsync = promisify(zlib.gunzip);
|
||||
|
||||
export const builderResponsePath = path.normalize(
|
||||
path.join(__dirname, '..', 'test-data', 'builder-response'),
|
||||
@ -45,9 +47,7 @@ export class BuilderMock extends NockMock {
|
||||
await opts.checkURI(uri);
|
||||
if (typeof requestBody === 'string') {
|
||||
const gzipped = Buffer.from(requestBody, 'hex');
|
||||
const gunzipped = await Bluebird.fromCallback<Buffer>((cb) => {
|
||||
zlib.gunzip(gzipped, cb);
|
||||
});
|
||||
const gunzipped = await gunzipAsync(gzipped);
|
||||
await opts.checkBuildRequestBody(gunzipped);
|
||||
} else {
|
||||
throw new Error(
|
||||
|
@ -205,12 +205,6 @@
|
||||
> Warning Entry 'main' not found in %1
|
||||
%1: node_modules/@oclif/core/package.json
|
||||
%2: build/commands/push/index.js
|
||||
> Warning Entry 'main' not found in %1
|
||||
%1: node_modules/@oclif/core/package.json
|
||||
%2: build/commands/release/export.js
|
||||
> Warning Entry 'main' not found in %1
|
||||
%1: node_modules/@oclif/core/package.json
|
||||
%2: build/commands/release/import.js
|
||||
> Warning Entry 'main' not found in %1
|
||||
%1: node_modules/@oclif/core/package.json
|
||||
%2: build/commands/release/index.js
|
||||
|
@ -205,12 +205,6 @@
|
||||
> Warning Entry 'main' not found in %1
|
||||
%1: node_modules/@oclif/core/package.json
|
||||
%2: build/commands/push/index.js
|
||||
> Warning Entry 'main' not found in %1
|
||||
%1: node_modules/@oclif/core/package.json
|
||||
%2: build/commands/release/export.js
|
||||
> Warning Entry 'main' not found in %1
|
||||
%1: node_modules/@oclif/core/package.json
|
||||
%2: build/commands/release/import.js
|
||||
> Warning Entry 'main' not found in %1
|
||||
%1: node_modules/@oclif/core/package.json
|
||||
%2: build/commands/release/index.js
|
||||
|
@ -205,12 +205,6 @@
|
||||
> Warning Entry 'main' not found in %1
|
||||
%1: node_modules/@oclif/core/package.json
|
||||
%2: build/commands/push/index.js
|
||||
> Warning Entry 'main' not found in %1
|
||||
%1: node_modules/@oclif/core/package.json
|
||||
%2: build/commands/release/export.js
|
||||
> Warning Entry 'main' not found in %1
|
||||
%1: node_modules/@oclif/core/package.json
|
||||
%2: build/commands/release/import.js
|
||||
> Warning Entry 'main' not found in %1
|
||||
%1: node_modules/@oclif/core/package.json
|
||||
%2: build/commands/release/index.js
|
||||
|
@ -205,12 +205,6 @@
|
||||
> Warning Entry 'main' not found in %1
|
||||
%1: node_modules/@oclif/core/package.json
|
||||
%2: build/commands/push/index.js
|
||||
> Warning Entry 'main' not found in %1
|
||||
%1: node_modules/@oclif/core/package.json
|
||||
%2: build/commands/release/export.js
|
||||
> Warning Entry 'main' not found in %1
|
||||
%1: node_modules/@oclif/core/package.json
|
||||
%2: build/commands/release/import.js
|
||||
> Warning Entry 'main' not found in %1
|
||||
%1: node_modules/@oclif/core/package.json
|
||||
%2: build/commands/release/index.js
|
||||
|
@ -205,12 +205,6 @@
|
||||
> Warning Entry 'main' not found in %1
|
||||
%1: node_modules\@oclif\core\package.json
|
||||
%2: build\commands\push\index.js
|
||||
> Warning Entry 'main' not found in %1
|
||||
%1: node_modules\@oclif\core\package.json
|
||||
%2: build\commands\release\export.js
|
||||
> Warning Entry 'main' not found in %1
|
||||
%1: node_modules\@oclif\core\package.json
|
||||
%2: build\commands\release\import.js
|
||||
> Warning Entry 'main' not found in %1
|
||||
%1: node_modules\@oclif\core\package.json
|
||||
%2: build\commands\release\index.js
|
||||
|
5
tests/utils.ts
Normal file
5
tests/utils.ts
Normal file
@ -0,0 +1,5 @@
|
||||
export async function delay(ms: number) {
|
||||
await new Promise((resolve) => {
|
||||
setTimeout(resolve, ms);
|
||||
});
|
||||
}
|
1
tests/utils/image-manager/fixtures/lorem.txt
Normal file
1
tests/utils/image-manager/fixtures/lorem.txt
Normal file
@ -0,0 +1 @@
|
||||
Lorem ipsum dolor sit amet
|
BIN
tests/utils/image-manager/fixtures/lorem.zip
Normal file
BIN
tests/utils/image-manager/fixtures/lorem.zip
Normal file
Binary file not shown.
568
tests/utils/image-manager/image-manager.spec.ts
Normal file
568
tests/utils/image-manager/image-manager.spec.ts
Normal file
@ -0,0 +1,568 @@
|
||||
import * as stream from 'stream';
|
||||
import { AssertionError, expect } from 'chai';
|
||||
import { stub } from 'sinon';
|
||||
import * as tmp from 'tmp';
|
||||
import { delay } from '../../utils';
|
||||
import * as fs from 'fs';
|
||||
import * as fsAsync from 'fs/promises';
|
||||
import * as stringToStream from 'string-to-stream';
|
||||
import { Writable as WritableStream } from 'stream';
|
||||
import * as imageManager from '../../../build/utils/image-manager';
|
||||
import { resolve, extname } from 'path';
|
||||
import * as mockFs from 'mock-fs';
|
||||
import * as rimraf from 'rimraf';
|
||||
import { promisify } from 'util';
|
||||
import * as os from 'os';
|
||||
|
||||
// Make sure we're all using literally the same instance of balena-sdk
|
||||
// so we can mock out methods called by the real code
|
||||
import { getBalenaSdk } from '../../../build/utils/lazy';
|
||||
const balena = getBalenaSdk();
|
||||
|
||||
const fsExistsAsync = promisify(fs.exists);
|
||||
const clean = async () => {
|
||||
await promisify(rimraf)(await balena.settings.get('cacheDirectory'));
|
||||
};
|
||||
|
||||
describe('image-manager', function () {
|
||||
describe('.getStream()', () => {
|
||||
describe('given the existing image', function () {
|
||||
beforeEach(function () {
|
||||
this.image = tmp.fileSync();
|
||||
fs.writeSync(this.image.fd, 'Cache image', 0, 'utf8');
|
||||
|
||||
this.cacheGetImagePathStub = stub(imageManager, 'getImagePath');
|
||||
return this.cacheGetImagePathStub.returns(
|
||||
Promise.resolve(this.image.name),
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
this.cacheGetImagePathStub.restore();
|
||||
return this.image.removeCallback();
|
||||
});
|
||||
|
||||
describe('given the image is fresh', function () {
|
||||
beforeEach(function () {
|
||||
this.cacheIsImageFresh = stub(imageManager, 'isImageFresh');
|
||||
return this.cacheIsImageFresh.returns(Promise.resolve(true));
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
return this.cacheIsImageFresh.restore();
|
||||
});
|
||||
|
||||
it('should eventually become a readable stream of the cached image', function (done) {
|
||||
this.timeout(5000);
|
||||
|
||||
void imageManager.getStream('raspberry-pi').then(function (stream) {
|
||||
let result = '';
|
||||
|
||||
stream.on('data', (chunk) => (result += chunk.toString()));
|
||||
|
||||
return stream.on('end', function () {
|
||||
expect(result).to.equal('Cache image');
|
||||
return done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('given the image is not fresh', function () {
|
||||
beforeEach(function () {
|
||||
this.cacheIsImageFresh = stub(imageManager, 'isImageFresh');
|
||||
return this.cacheIsImageFresh.returns(Promise.resolve(false));
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
return this.cacheIsImageFresh.restore();
|
||||
});
|
||||
|
||||
// Skipping test because we keep getting `Cache image` instead of `Download image`
|
||||
describe.skip('given a valid download endpoint', function () {
|
||||
beforeEach(function () {
|
||||
this.osDownloadStub = stub(balena.models.os, 'download');
|
||||
this.osDownloadStub.returns(
|
||||
Promise.resolve(stringToStream('Download image')),
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
this.osDownloadStub.restore();
|
||||
});
|
||||
|
||||
it('should eventually become a readable stream of the download image and save a backup copy', function (done) {
|
||||
void imageManager.getStream('raspberry-pi').then((stream) => {
|
||||
let result = '';
|
||||
|
||||
stream.on('data', (chunk) => (result += chunk));
|
||||
|
||||
stream.on('end', async () => {
|
||||
expect(result).to.equal('Download image');
|
||||
const contents = await fsAsync.readFile(this.image.name, {
|
||||
encoding: 'utf8',
|
||||
});
|
||||
expect(contents).to.equal('Download image');
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should be able to read from the stream after a slight delay', function (done) {
|
||||
void imageManager.getStream('raspberry-pi').then(async (s) => {
|
||||
await delay(200);
|
||||
|
||||
const pass = new stream.PassThrough();
|
||||
s.pipe(pass);
|
||||
|
||||
let result = '';
|
||||
|
||||
pass.on('data', (chunk) => (result += chunk));
|
||||
|
||||
pass.on('end', function () {
|
||||
expect(result).to.equal('Download image');
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('given a failing download', function () {
|
||||
beforeEach(function () {
|
||||
this.osDownloadStream = new stream.PassThrough();
|
||||
this.osDownloadStub = stub(balena.models.os, 'download');
|
||||
this.osDownloadStub.returns(Promise.resolve(this.osDownloadStream));
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
this.osDownloadStub.restore();
|
||||
});
|
||||
|
||||
it('should clean up the in progress cached stream if an error occurs', function (done) {
|
||||
if (os.platform() === 'win32') {
|
||||
// Skipping test on Windows because we get `EPERM: operation not permitted, rename` for `getImageWritableStream` on the windows runner
|
||||
this.skip();
|
||||
}
|
||||
void imageManager.getStream('raspberry-pi').then((stream) => {
|
||||
stream.on('data', () => {
|
||||
// After the first chunk, error
|
||||
return this.osDownloadStream.emit('error');
|
||||
});
|
||||
|
||||
stream.on('error', async () => {
|
||||
const contents = await fsAsync
|
||||
.stat(this.image.name + '.inprogress')
|
||||
.then(function () {
|
||||
throw new AssertionError(
|
||||
'Image cache should be deleted on failure',
|
||||
);
|
||||
})
|
||||
.catch((err) => {
|
||||
if (err.code !== 'ENOENT') {
|
||||
throw err;
|
||||
}
|
||||
return fsAsync.readFile(this.image.name, {
|
||||
encoding: 'utf8',
|
||||
});
|
||||
});
|
||||
expect(contents).to.equal('Cache image');
|
||||
done();
|
||||
});
|
||||
|
||||
stringToStream('Download image').pipe(this.osDownloadStream);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('given a stream with the mime property', async function () {
|
||||
beforeEach(function () {
|
||||
this.osDownloadStub = stub(balena.models.os, 'download');
|
||||
const message = 'Lorem ipsum dolor sit amet';
|
||||
const mockResultStream = stringToStream(message) as ReturnType<
|
||||
typeof stringToStream
|
||||
> & {
|
||||
mime?: string;
|
||||
};
|
||||
mockResultStream.mime = 'application/zip';
|
||||
this.osDownloadStub.returns(Promise.resolve(mockResultStream));
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
this.osDownloadStub.restore();
|
||||
});
|
||||
|
||||
it('should preserve the property', () =>
|
||||
imageManager
|
||||
.getStream('raspberry-pi')
|
||||
.then((resultStream) =>
|
||||
expect(resultStream.mime).to.equal('application/zip'),
|
||||
));
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('.getImagePath()', () => {
|
||||
describe('given a cache directory', function () {
|
||||
beforeEach(function () {
|
||||
this.balenaSettingsGetStub = stub(balena.settings, 'get');
|
||||
|
||||
this.balenaSettingsGetStub
|
||||
.withArgs('cacheDirectory')
|
||||
.returns(
|
||||
Promise.resolve(
|
||||
os.platform() === 'win32'
|
||||
? 'C:\\Users\\johndoe\\_balena\\cache'
|
||||
: '/Users/johndoe/.balena/cache',
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
this.balenaSettingsGetStub.restore();
|
||||
});
|
||||
|
||||
describe('given valid slugs', function () {
|
||||
beforeEach(function () {
|
||||
this.getDeviceTypeManifestBySlugStub = stub(
|
||||
balena.models.config,
|
||||
'getDeviceTypeManifestBySlug',
|
||||
);
|
||||
this.getDeviceTypeManifestBySlugStub.withArgs('raspberry-pi').returns(
|
||||
Promise.resolve({
|
||||
yocto: {
|
||||
fstype: 'resin-sdcard',
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
this.getDeviceTypeManifestBySlugStub.withArgs('intel-edison').returns(
|
||||
Promise.resolve({
|
||||
yocto: {
|
||||
fstype: 'zip',
|
||||
},
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
this.getDeviceTypeManifestBySlugStub.restore();
|
||||
});
|
||||
|
||||
it('should eventually equal an absolute path', async () => {
|
||||
await imageManager
|
||||
.getImagePath('raspberry-pi', '1.2.3')
|
||||
.then(function (imagePath) {
|
||||
const isAbsolute = imagePath === resolve(imagePath);
|
||||
expect(isAbsolute).to.be.true;
|
||||
});
|
||||
});
|
||||
|
||||
it('should eventually equal the correct path', async function () {
|
||||
const result = await imageManager.getImagePath(
|
||||
'raspberry-pi',
|
||||
'1.2.3',
|
||||
);
|
||||
expect(result).to.equal(
|
||||
os.platform() === 'win32'
|
||||
? 'C:\\Users\\johndoe\\_balena\\cache\\raspberry-pi-v1.2.3.img'
|
||||
: '/Users/johndoe/.balena/cache/raspberry-pi-v1.2.3.img',
|
||||
);
|
||||
});
|
||||
|
||||
it('should use a zip extension for directory images', async () => {
|
||||
const imagePath = await imageManager.getImagePath(
|
||||
'intel-edison',
|
||||
'1.2.3',
|
||||
);
|
||||
expect(extname(imagePath)).to.equal('.zip');
|
||||
});
|
||||
|
||||
it('given invalid version should be rejected', async function () {
|
||||
const promise = imageManager.getImagePath('intel-edison', 'DOUGH');
|
||||
await expect(promise).to.be.eventually.rejectedWith(
|
||||
'Invalid version number',
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('.isImageFresh()', () => {
|
||||
describe('given the raspberry-pi manifest', function () {
|
||||
beforeEach(function () {
|
||||
this.getDeviceTypeManifestBySlugStub = stub(
|
||||
balena.models.config,
|
||||
'getDeviceTypeManifestBySlug',
|
||||
);
|
||||
this.getDeviceTypeManifestBySlugStub.returns(
|
||||
Promise.resolve({
|
||||
yocto: {
|
||||
fstype: 'balena-sdcard',
|
||||
},
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
this.getDeviceTypeManifestBySlugStub.restore();
|
||||
});
|
||||
|
||||
describe('given the file does not exist', function () {
|
||||
beforeEach(function () {
|
||||
this.utilsGetFileCreatedDate = stub(
|
||||
imageManager,
|
||||
'getFileCreatedDate',
|
||||
);
|
||||
this.utilsGetFileCreatedDate.returns(
|
||||
Promise.reject(new Error("ENOENT, stat 'raspberry-pi'")),
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
this.utilsGetFileCreatedDate.restore();
|
||||
});
|
||||
|
||||
it('should return false', async function () {
|
||||
expect(await imageManager.isImageFresh('raspberry-pi', '1.2.3')).to.be
|
||||
.false;
|
||||
});
|
||||
});
|
||||
|
||||
describe('given a fixed created time', function () {
|
||||
beforeEach(function () {
|
||||
this.utilsGetFileCreatedDate = stub(
|
||||
imageManager,
|
||||
'getFileCreatedDate',
|
||||
);
|
||||
this.utilsGetFileCreatedDate.returns(
|
||||
Promise.resolve(new Date('2014-01-01T00:00:00.000Z')),
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
this.utilsGetFileCreatedDate.restore();
|
||||
});
|
||||
|
||||
describe('given the file was created before the os last modified time', function () {
|
||||
beforeEach(function () {
|
||||
this.osGetLastModified = stub(balena.models.os, 'getLastModified');
|
||||
this.osGetLastModified.returns(
|
||||
Promise.resolve(new Date('2014-02-01T00:00:00.000Z')),
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
this.osGetLastModified.restore();
|
||||
});
|
||||
|
||||
it('should return false', function () {
|
||||
const promise = imageManager.isImageFresh('raspberry-pi', '1.2.3');
|
||||
return expect(promise).to.eventually.be.false;
|
||||
});
|
||||
});
|
||||
|
||||
describe('given the file was created after the os last modified time', function () {
|
||||
beforeEach(function () {
|
||||
this.osGetLastModified = stub(balena.models.os, 'getLastModified');
|
||||
this.osGetLastModified.returns(
|
||||
Promise.resolve(new Date('2013-01-01T00:00:00.000Z')),
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
this.osGetLastModified.restore();
|
||||
});
|
||||
|
||||
it('should return true', function () {
|
||||
const promise = imageManager.isImageFresh('raspberry-pi', '1.2.3');
|
||||
return expect(promise).to.eventually.be.true;
|
||||
});
|
||||
});
|
||||
|
||||
describe('given the file was created just at the os last modified time', function () {
|
||||
beforeEach(function () {
|
||||
this.osGetLastModified = stub(balena.models.os, 'getLastModified');
|
||||
this.osGetLastModified.returns(
|
||||
Promise.resolve(new Date('2014-00-01T00:00:00.000Z')),
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
this.osGetLastModified.restore();
|
||||
});
|
||||
|
||||
it('should return false', function () {
|
||||
const promise = imageManager.isImageFresh('raspberry-pi', '1.2.3');
|
||||
return expect(promise).to.eventually.be.false;
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('.getImage()', () => {
|
||||
describe('given an existing image', function () {
|
||||
beforeEach(function () {
|
||||
this.image = tmp.fileSync();
|
||||
fs.writeSync(this.image.fd, 'Lorem ipsum dolor sit amet', 0, 'utf8');
|
||||
|
||||
this.cacheGetImagePathStub = stub(imageManager, 'getImagePath');
|
||||
this.cacheGetImagePathStub.returns(Promise.resolve(this.image.name));
|
||||
});
|
||||
|
||||
afterEach(function (done) {
|
||||
this.cacheGetImagePathStub.restore();
|
||||
fs.unlink(this.image.name, done);
|
||||
});
|
||||
|
||||
it('should return a stream to the image', function (done) {
|
||||
void imageManager
|
||||
.getImage('lorem-ipsum', '1.2.3')
|
||||
.then(function (stream) {
|
||||
let result = '';
|
||||
|
||||
stream.on('data', (chunk) => (result += chunk));
|
||||
|
||||
stream.on('end', function () {
|
||||
expect(result).to.equal('Lorem ipsum dolor sit amet');
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should contain the mime property', () =>
|
||||
imageManager
|
||||
.getImage('lorem-ipsum', '1.2.3')
|
||||
.then((stream) =>
|
||||
expect(stream.mime).to.equal('application/octet-stream'),
|
||||
));
|
||||
});
|
||||
});
|
||||
|
||||
describe('.getImageWritableStream()', () => {
|
||||
describe('given the valid image path', function () {
|
||||
beforeEach(function () {
|
||||
this.image = tmp.fileSync();
|
||||
this.cacheGetImagePathStub = stub(imageManager, 'getImagePath');
|
||||
this.cacheGetImagePathStub.returns(Promise.resolve(this.image.name));
|
||||
});
|
||||
|
||||
afterEach(function (done) {
|
||||
this.cacheGetImagePathStub.restore();
|
||||
fs.unlink(this.image.name, done);
|
||||
});
|
||||
|
||||
it('should return a writable stream', () =>
|
||||
imageManager
|
||||
.getImageWritableStream('raspberry-pi', '1.2.3')
|
||||
.then((stream) =>
|
||||
expect(stream).to.be.an.instanceof(WritableStream),
|
||||
));
|
||||
|
||||
it('should allow writing to the stream', function (done) {
|
||||
if (os.platform() === 'win32') {
|
||||
// Skipping test on Windows because we get `EPERM: operation not permitted, rename` for `getImageWritableStream` on the windows runner
|
||||
this.skip();
|
||||
}
|
||||
void imageManager
|
||||
.getImageWritableStream('raspberry-pi', '1.2.3')
|
||||
.then((stream) => {
|
||||
const stringStream = stringToStream('Lorem ipsum dolor sit amet');
|
||||
stringStream.pipe(stream);
|
||||
stream.on('finish', async () => {
|
||||
await stream.persistCache();
|
||||
const contents = await fsAsync.readFile(this.image.name, {
|
||||
encoding: 'utf8',
|
||||
});
|
||||
expect(contents).to.equal('Lorem ipsum dolor sit amet');
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('.getFileCreatedDate()', function () {
|
||||
describe('given the file exists', function () {
|
||||
beforeEach(function () {
|
||||
this.date = new Date(2014, 1, 1);
|
||||
this.fsStatStub = stub(fs.promises, 'stat');
|
||||
this.fsStatStub
|
||||
.withArgs('foo')
|
||||
.returns(Promise.resolve({ ctime: this.date }));
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
this.fsStatStub.restore();
|
||||
});
|
||||
|
||||
it('should eventually equal the created time in milliseconds', async function () {
|
||||
const promise = imageManager.getFileCreatedDate('foo');
|
||||
await expect(promise).to.eventually.equal(this.date);
|
||||
});
|
||||
});
|
||||
|
||||
describe('given the file does not exist', function () {
|
||||
beforeEach(function () {
|
||||
this.fsStatStub = stub(fs.promises, 'stat');
|
||||
this.fsStatStub
|
||||
.withArgs('foo')
|
||||
.returns(Promise.reject(new Error("ENOENT, stat 'foo'")));
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
this.fsStatStub.restore();
|
||||
});
|
||||
|
||||
it('should be rejected with an error', async function () {
|
||||
const promise = imageManager.getFileCreatedDate('foo');
|
||||
await expect(promise).to.be.rejectedWith('ENOENT');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('.clean()', function () {
|
||||
describe('given the cache with saved images', function () {
|
||||
beforeEach(async function () {
|
||||
this.cacheDirectory = await balena.settings.get('cacheDirectory');
|
||||
mockFs({
|
||||
[this.cacheDirectory]: {
|
||||
'raspberry-pi': 'Raspberry Pi Image',
|
||||
'intel-edison': 'Intel Edison Image',
|
||||
parallela: 'Parallela Image',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
mockFs.restore();
|
||||
});
|
||||
|
||||
it('should remove the cache directory completely', async function () {
|
||||
const exists = await fsExistsAsync(this.cacheDirectory);
|
||||
expect(exists).to.be.true;
|
||||
await clean();
|
||||
const exists2 = await fsExistsAsync(this.cacheDirectory);
|
||||
expect(exists2).to.be.false;
|
||||
});
|
||||
});
|
||||
|
||||
describe('given no cache', function () {
|
||||
beforeEach(async function () {
|
||||
this.cacheDirectory = await balena.settings.get('cacheDirectory');
|
||||
mockFs({});
|
||||
});
|
||||
|
||||
afterEach(() => mockFs.restore());
|
||||
|
||||
it('should keep the cache directory removed', async function () {
|
||||
const exists = await fsExistsAsync(this.cacheDirectory);
|
||||
expect(exists).to.be.false;
|
||||
await clean();
|
||||
const exists2 = await fsExistsAsync(this.cacheDirectory);
|
||||
expect(exists2).to.be.false;
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
13
typings/balena-device-init/index.d.ts
vendored
13
typings/balena-device-init/index.d.ts
vendored
@ -17,7 +17,6 @@
|
||||
|
||||
declare module 'balena-device-init' {
|
||||
import { DeviceTypeJson } from 'balena-sdk';
|
||||
import type * as Bluebird from 'bluebird';
|
||||
|
||||
interface OperationState {
|
||||
operation:
|
||||
@ -78,25 +77,29 @@ declare module 'balena-device-init' {
|
||||
on(event: 'error', callback: (error: Error) => void): void;
|
||||
}
|
||||
|
||||
// As of writing this, these are Bluebird promises, but we are typing then
|
||||
// as normal Promises so that we do not rely on Bluebird specific methods,
|
||||
// so that the CLI will not require any change once the package drops Bluebird.
|
||||
|
||||
export function configure(
|
||||
image: string,
|
||||
manifest: BalenaSdk.DeviceTypeJson.DeviceType.DeviceType,
|
||||
config: object,
|
||||
options?: object,
|
||||
): Bluebird<InitializeEmitter>;
|
||||
): Promise<InitializeEmitter>;
|
||||
|
||||
export function initialize(
|
||||
image: string,
|
||||
manifest: BalenaSdk.DeviceTypeJson.DeviceType.DeviceType,
|
||||
config: object,
|
||||
): Bluebird<InitializeEmitter>;
|
||||
): Promise<InitializeEmitter>;
|
||||
|
||||
export function getImageOsVersion(
|
||||
image: string,
|
||||
manifest: BalenaSdk.DeviceTypeJson.DeviceType.DeviceType,
|
||||
): Bluebird<string | null>;
|
||||
): Promise<string | null>;
|
||||
|
||||
export function getImageManifest(
|
||||
image: string,
|
||||
): Bluebird<BalenaSdk.DeviceTypeJson.DeviceType.DeviceType | null>;
|
||||
): Promise<BalenaSdk.DeviceTypeJson.DeviceType.DeviceType | null>;
|
||||
}
|
||||
|
10
typings/resin-cli-form/index.d.ts
vendored
10
typings/resin-cli-form/index.d.ts
vendored
@ -16,8 +16,6 @@
|
||||
*/
|
||||
|
||||
declare module 'resin-cli-form' {
|
||||
import Bluebird = require('bluebird');
|
||||
|
||||
export type TypeOrPromiseLike<T> = T | PromiseLike<T>;
|
||||
|
||||
export type Validate = (
|
||||
@ -43,9 +41,13 @@ declare module 'resin-cli-form' {
|
||||
validate?: Validate;
|
||||
}
|
||||
|
||||
export const ask: <T = string>(options: AskOptions<T>) => Bluebird<T>;
|
||||
// As of writing this, these are Bluebird promises, but we are typing then
|
||||
// as normal Promises so that we do not rely on Bluebird specific methods,
|
||||
// so that the CLI will not require any change once the package drops Bluebird.
|
||||
|
||||
export const ask: <T = string>(options: AskOptions<T>) => Promise<T>;
|
||||
export const run: <T = any>(
|
||||
questions?: RunQuestion[],
|
||||
extraOptions?: { override?: object },
|
||||
) => Bluebird<T>;
|
||||
) => Promise<T>;
|
||||
}
|
||||
|
18
typings/resin.io/index.d.ts
vendored
18
typings/resin.io/index.d.ts
vendored
@ -1,18 +0,0 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2019 Balena Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
declare module '@resin.io/valid-email';
|
Reference in New Issue
Block a user