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:
Marios Balamatsias 2020-12-16 16:42:25 +02:00
parent 3bff569758
commit 34557e35ee
3 changed files with 199 additions and 75 deletions

View File

@ -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

View File

@ -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,19 +271,81 @@ export default class PushCmd extends Command {
const buildTarget = await this.getBuildTarget(appOrDevice);
switch (buildTarget) {
case BuildTarget.Cloud:
await this.pushToCloud(
options,
sdk,
appOrDevice,
dockerfilePath,
registrySecrets,
convertEol,
source,
nogitignore,
);
break;
case BuildTarget.Device:
await this.pushToDevice(
options,
sdk,
appOrDevice,
dockerfilePath,
registrySecrets,
convertEol,
source,
nogitignore,
);
break;
default:
throw new ExpectedError(stripIndent`
Build target not recognized. Please provide either an application name or
device IP address.`);
}
}
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 = ['nolive', 'service', 'system', 'env'];
const localOnlyOptions: Array<keyof FlagsDef> = [
'nolive',
'service',
'system',
'env',
];
this.checkInvalidOptions(
localOnlyOptions,
options,
'is only valid when pushing to a local mode device',
);
localOnlyOptions.forEach((opt) => {
// @ts-ignore : Not sure why typescript wont let me do this?
if (options[opt]) {
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(
`The --${opt} flag is only valid when pushing to a local mode device`,
`Error: --release-tag keys cannot contain whitespaces`,
);
}
});
if (releaseTagKeys.length !== releaseTagValues.length) {
releaseTagValues.push('');
}
const app = appOrDevice;
await Command.checkLoggedIn();
@ -300,10 +374,41 @@ export default class PushCmd extends Command {
sdk,
opts,
};
await remote.startRemoteBuild(args);
break;
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',
);
case BuildTarget.Device:
const deviceDeploy = await import('../utils/device/deploy');
const device = appOrDevice;
const servicesToDisplay = options.service;
@ -335,13 +440,6 @@ export default class PushCmd extends Command {
throw e;
}
}
break;
default:
throw new ExpectedError(stripIndent`
Build target not recognized. Please provide either an application name or
device IP address.`);
}
}
async getBuildTarget(appOrDevice: string): Promise<BuildTarget | null> {
@ -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}`);
}
});
}
}

View File

@ -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!}`);