mirror of
https://github.com/balena-io/balena-cli.git
synced 2024-12-20 06:07:55 +00:00
push: Add --release-tag flag
You can have 0 or multiple keys without values, if you use values then you should have as many values as you have keys. If you don't want to set a value for a key set its value to "" (bash, cmd.exe) or '""' (powershell). Connects-to: #892 Change-type: minor Signed-off-by: Marios Balamatsias <mbalamatsias@gmail.com>
This commit is contained in:
parent
3bff569758
commit
34557e35ee
@ -2544,6 +2544,7 @@ Examples:
|
||||
$ balena push myApp
|
||||
$ balena push myApp --source <source directory>
|
||||
$ balena push myApp -s <source directory>
|
||||
$ balena push myApp --release-tag key1 "" key2 "value2 with spaces"
|
||||
|
||||
$ balena push 10.0.0.1
|
||||
$ balena push 10.0.0.1 --source <source directory>
|
||||
@ -2658,6 +2659,12 @@ Consider .gitignore files in addition to the .dockerignore file. This reverts
|
||||
to the CLI v11 behavior/implementation (deprecated) if compatibility is
|
||||
required until your project can be adapted.
|
||||
|
||||
#### --release-tag RELEASE-TAG
|
||||
|
||||
Set release tags if the push to a cloud application is successful. Multiple
|
||||
arguments may be provided, alternating tag keys and values (see examples).
|
||||
Hint: Empty values may be specified with "" (bash, cmd.exe) or '""' (PowerShell).
|
||||
|
||||
# Settings
|
||||
|
||||
## settings
|
||||
|
@ -22,6 +22,7 @@ import { getBalenaSdk, stripIndent } from '../utils/lazy';
|
||||
import { dockerignoreHelp, registrySecretsHelp } from '../utils/messages';
|
||||
import type { BalenaSDK, Application, Organization } from 'balena-sdk';
|
||||
import { ExpectedError, instanceOf } from '../errors';
|
||||
import type { RegistrySecrets } from 'resin-multibuild';
|
||||
|
||||
enum BuildTarget {
|
||||
Cloud,
|
||||
@ -46,6 +47,7 @@ interface FlagsDef {
|
||||
'convert-eol'?: boolean;
|
||||
'noconvert-eol'?: boolean;
|
||||
'multi-dockerignore'?: boolean;
|
||||
'release-tag'?: string[];
|
||||
help: void;
|
||||
}
|
||||
|
||||
@ -92,6 +94,7 @@ export default class PushCmd extends Command {
|
||||
'$ balena push myApp',
|
||||
'$ balena push myApp --source <source directory>',
|
||||
'$ balena push myApp -s <source directory>',
|
||||
'$ balena push myApp --release-tag key1 "" key2 "value2 with spaces"',
|
||||
'',
|
||||
'$ balena push 10.0.0.1',
|
||||
'$ balena push 10.0.0.1 --source <source directory>',
|
||||
@ -224,6 +227,15 @@ export default class PushCmd extends Command {
|
||||
char: 'g',
|
||||
exclusive: ['multi-dockerignore'],
|
||||
}),
|
||||
'release-tag': flags.string({
|
||||
description: stripIndent`
|
||||
Set release tags if the push to a cloud application is successful. Multiple
|
||||
arguments may be provided, alternating tag keys and values (see examples).
|
||||
Hint: Empty values may be specified with "" (bash, cmd.exe) or '""' (PowerShell).
|
||||
`,
|
||||
multiple: true,
|
||||
exclusive: ['detached'],
|
||||
}),
|
||||
help: cf.help,
|
||||
};
|
||||
|
||||
@ -259,82 +271,29 @@ export default class PushCmd extends Command {
|
||||
const buildTarget = await this.getBuildTarget(appOrDevice);
|
||||
switch (buildTarget) {
|
||||
case BuildTarget.Cloud:
|
||||
const remote = await import('../utils/remote-build');
|
||||
|
||||
// Check for invalid options
|
||||
const localOnlyOptions = ['nolive', 'service', 'system', 'env'];
|
||||
|
||||
localOnlyOptions.forEach((opt) => {
|
||||
// @ts-ignore : Not sure why typescript wont let me do this?
|
||||
if (options[opt]) {
|
||||
throw new ExpectedError(
|
||||
`The --${opt} flag is only valid when pushing to a local mode device`,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
const app = appOrDevice;
|
||||
await Command.checkLoggedIn();
|
||||
const [token, baseUrl, owner] = await Promise.all([
|
||||
sdk.auth.getToken(),
|
||||
sdk.settings.get('balenaUrl'),
|
||||
this.getAppOwner(sdk, app),
|
||||
]);
|
||||
|
||||
const opts = {
|
||||
dockerfilePath,
|
||||
emulated: options.emulated || false,
|
||||
multiDockerignore: options['multi-dockerignore'] || false,
|
||||
nocache: options.nocache || false,
|
||||
registrySecrets,
|
||||
headless: options.detached || false,
|
||||
convertEol,
|
||||
};
|
||||
const args = {
|
||||
app,
|
||||
owner,
|
||||
source,
|
||||
auth: token,
|
||||
baseUrl,
|
||||
nogitignore,
|
||||
await this.pushToCloud(
|
||||
options,
|
||||
sdk,
|
||||
opts,
|
||||
};
|
||||
await remote.startRemoteBuild(args);
|
||||
appOrDevice,
|
||||
dockerfilePath,
|
||||
registrySecrets,
|
||||
convertEol,
|
||||
source,
|
||||
nogitignore,
|
||||
);
|
||||
break;
|
||||
|
||||
case BuildTarget.Device:
|
||||
const deviceDeploy = await import('../utils/device/deploy');
|
||||
const device = appOrDevice;
|
||||
const servicesToDisplay = options.service;
|
||||
|
||||
// TODO: Support passing a different port
|
||||
try {
|
||||
await deviceDeploy.deployToDevice({
|
||||
source,
|
||||
deviceHost: device,
|
||||
dockerfilePath,
|
||||
registrySecrets,
|
||||
multiDockerignore: options['multi-dockerignore'] || false,
|
||||
nocache: options.nocache || false,
|
||||
pull: options.pull || false,
|
||||
nogitignore,
|
||||
noParentCheck: options['noparent-check'] || false,
|
||||
nolive: options.nolive || false,
|
||||
detached: options.detached || false,
|
||||
services: servicesToDisplay,
|
||||
system: options.system || false,
|
||||
env: options.env || [],
|
||||
convertEol,
|
||||
});
|
||||
} catch (e) {
|
||||
const { BuildError } = await import('../utils/device/errors');
|
||||
if (instanceOf(e, BuildError)) {
|
||||
throw new ExpectedError(e.toString());
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
await this.pushToDevice(
|
||||
options,
|
||||
sdk,
|
||||
appOrDevice,
|
||||
dockerfilePath,
|
||||
registrySecrets,
|
||||
convertEol,
|
||||
source,
|
||||
nogitignore,
|
||||
);
|
||||
break;
|
||||
|
||||
default:
|
||||
@ -344,6 +303,145 @@ export default class PushCmd extends Command {
|
||||
}
|
||||
}
|
||||
|
||||
async pushToCloud(
|
||||
options: FlagsDef,
|
||||
sdk: BalenaSDK,
|
||||
appOrDevice: string,
|
||||
dockerfilePath: string,
|
||||
registrySecrets: RegistrySecrets,
|
||||
convertEol: boolean,
|
||||
source: string,
|
||||
nogitignore: boolean,
|
||||
) {
|
||||
const _ = await import('lodash');
|
||||
const remote = await import('../utils/remote-build');
|
||||
|
||||
// Check for invalid options
|
||||
const localOnlyOptions: Array<keyof FlagsDef> = [
|
||||
'nolive',
|
||||
'service',
|
||||
'system',
|
||||
'env',
|
||||
];
|
||||
this.checkInvalidOptions(
|
||||
localOnlyOptions,
|
||||
options,
|
||||
'is only valid when pushing to a local mode device',
|
||||
);
|
||||
|
||||
const releaseTags = options['release-tag'] ?? [];
|
||||
const releaseTagKeys = releaseTags.filter((_v, i) => i % 2 === 0);
|
||||
const releaseTagValues = releaseTags.filter((_v, i) => i % 2 === 1);
|
||||
|
||||
releaseTagKeys.forEach((key) => {
|
||||
if (key === '') {
|
||||
throw new ExpectedError(`Error: --release-tag keys cannot be empty`);
|
||||
}
|
||||
if (/\s/.test(key)) {
|
||||
throw new ExpectedError(
|
||||
`Error: --release-tag keys cannot contain whitespaces`,
|
||||
);
|
||||
}
|
||||
});
|
||||
if (releaseTagKeys.length !== releaseTagValues.length) {
|
||||
releaseTagValues.push('');
|
||||
}
|
||||
|
||||
const app = appOrDevice;
|
||||
await Command.checkLoggedIn();
|
||||
const [token, baseUrl, owner] = await Promise.all([
|
||||
sdk.auth.getToken(),
|
||||
sdk.settings.get('balenaUrl'),
|
||||
this.getAppOwner(sdk, app),
|
||||
]);
|
||||
|
||||
const opts = {
|
||||
dockerfilePath,
|
||||
emulated: options.emulated || false,
|
||||
multiDockerignore: options['multi-dockerignore'] || false,
|
||||
nocache: options.nocache || false,
|
||||
registrySecrets,
|
||||
headless: options.detached || false,
|
||||
convertEol,
|
||||
};
|
||||
const args = {
|
||||
app,
|
||||
owner,
|
||||
source,
|
||||
auth: token,
|
||||
baseUrl,
|
||||
nogitignore,
|
||||
sdk,
|
||||
opts,
|
||||
};
|
||||
const releaseId = await remote.startRemoteBuild(args);
|
||||
if (releaseId) {
|
||||
// Above we have checked that releaseTagKeys and releaseTagValues are of the same size
|
||||
await Promise.all(
|
||||
(_.zip(releaseTagKeys, releaseTagValues) as Array<
|
||||
[string, string]
|
||||
>).map(async ([key, value]) => {
|
||||
await sdk.models.release.tags.set(releaseId, key, value);
|
||||
}),
|
||||
);
|
||||
} else if (releaseTagKeys.length > 0) {
|
||||
throw new Error(stripIndent`
|
||||
A release ID could not be parsed out of the builder's output.
|
||||
As a result, the release tags have not been set.`);
|
||||
}
|
||||
}
|
||||
|
||||
async pushToDevice(
|
||||
options: FlagsDef,
|
||||
_sdk: BalenaSDK,
|
||||
appOrDevice: string,
|
||||
dockerfilePath: string,
|
||||
registrySecrets: RegistrySecrets,
|
||||
convertEol: boolean,
|
||||
source: string,
|
||||
nogitignore: boolean,
|
||||
) {
|
||||
// Check for invalid options
|
||||
const remoteOnlyOptions: Array<keyof FlagsDef> = ['release-tag'];
|
||||
this.checkInvalidOptions(
|
||||
remoteOnlyOptions,
|
||||
options,
|
||||
'is only valid when pushing to an application',
|
||||
);
|
||||
|
||||
const deviceDeploy = await import('../utils/device/deploy');
|
||||
const device = appOrDevice;
|
||||
const servicesToDisplay = options.service;
|
||||
|
||||
// TODO: Support passing a different port
|
||||
try {
|
||||
await deviceDeploy.deployToDevice({
|
||||
source,
|
||||
deviceHost: device,
|
||||
dockerfilePath,
|
||||
registrySecrets,
|
||||
multiDockerignore: options['multi-dockerignore'] || false,
|
||||
nocache: options.nocache || false,
|
||||
pull: options.pull || false,
|
||||
nogitignore,
|
||||
noParentCheck: options['noparent-check'] || false,
|
||||
nolive: options.nolive || false,
|
||||
detached: options.detached || false,
|
||||
services: servicesToDisplay,
|
||||
system: options.system || false,
|
||||
env: options.env || [],
|
||||
convertEol,
|
||||
});
|
||||
} catch (e) {
|
||||
const { BuildError } = await import('../utils/device/errors');
|
||||
if (instanceOf(e, BuildError)) {
|
||||
throw new ExpectedError(e.toString());
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async getBuildTarget(appOrDevice: string): Promise<BuildTarget | null> {
|
||||
const {
|
||||
validateApplicationName,
|
||||
@ -416,4 +514,16 @@ export default class PushCmd extends Command {
|
||||
|
||||
return selected.extra;
|
||||
}
|
||||
|
||||
checkInvalidOptions(
|
||||
invalidOptions: Array<keyof FlagsDef>,
|
||||
options: FlagsDef,
|
||||
errorMessage: string,
|
||||
) {
|
||||
invalidOptions.forEach((opt) => {
|
||||
if (options[opt]) {
|
||||
throw new ExpectedError(`The --${opt} flag ${errorMessage}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -108,7 +108,9 @@ async function getBuilderEndpoint(
|
||||
return `${builderUrl}/v3/build?${args}`;
|
||||
}
|
||||
|
||||
export async function startRemoteBuild(build: RemoteBuild): Promise<void> {
|
||||
export async function startRemoteBuild(
|
||||
build: RemoteBuild,
|
||||
): Promise<number | undefined> {
|
||||
const [buildRequest, stream] = await getRemoteBuildStream(build);
|
||||
|
||||
// Setup CTRL-C handler so the user can interrupt the build
|
||||
@ -133,7 +135,7 @@ export async function startRemoteBuild(build: RemoteBuild): Promise<void> {
|
||||
|
||||
try {
|
||||
if (build.opts.headless) {
|
||||
await handleHeadlessBuildStream(stream);
|
||||
await handleHeadlessBuildStream(build, stream);
|
||||
} else {
|
||||
await handleRemoteBuildStream(build, stream);
|
||||
}
|
||||
@ -142,6 +144,7 @@ export async function startRemoteBuild(build: RemoteBuild): Promise<void> {
|
||||
globalLogger.outputDeferredMessages();
|
||||
await cancellationPromise;
|
||||
}
|
||||
return build.releaseId;
|
||||
}
|
||||
|
||||
async function handleRemoteBuildStream(
|
||||
@ -159,7 +162,10 @@ async function handleRemoteBuildStream(
|
||||
}
|
||||
}
|
||||
|
||||
async function handleHeadlessBuildStream(stream: Stream.Stream) {
|
||||
async function handleHeadlessBuildStream(
|
||||
build: RemoteBuild,
|
||||
stream: Stream.Stream,
|
||||
) {
|
||||
// We're running a headless build, which means we'll
|
||||
// get a single object back, detailing if the build has
|
||||
// been started
|
||||
@ -182,6 +188,7 @@ async function handleHeadlessBuildStream(stream: Stream.Stream) {
|
||||
if (message.started) {
|
||||
console.log('Build successfully started');
|
||||
console.log(` Release ID: ${message.releaseId!}`);
|
||||
build.releaseId = message.releaseId;
|
||||
} else {
|
||||
console.log('Failed to start remote build');
|
||||
console.log(` Error: ${message.error!}`);
|
||||
|
Loading…
Reference in New Issue
Block a user