mirror of
https://github.com/balena-io/balena-cli.git
synced 2024-12-22 23:12:22 +00:00
Merge pull request #2154 from balena-io/add_release_tags_to_deploy
Add release-tag on deploy command
This commit is contained in:
commit
743de66138
@ -3052,6 +3052,7 @@ Examples:
|
|||||||
$ balena deploy myApp
|
$ balena deploy myApp
|
||||||
$ balena deploy myApp --build --source myBuildDir/
|
$ balena deploy myApp --build --source myBuildDir/
|
||||||
$ balena deploy myApp myApp/myImage
|
$ balena deploy myApp myApp/myImage
|
||||||
|
$ balena deploy myApp myApp/myImage --release-tag key1 "" key2 "value2 with spaces"
|
||||||
|
|
||||||
### Arguments
|
### Arguments
|
||||||
|
|
||||||
@ -3077,6 +3078,12 @@ force a rebuild before deploy
|
|||||||
|
|
||||||
don't upload build logs to the dashboard with image (if building)
|
don't upload build logs to the dashboard with image (if building)
|
||||||
|
|
||||||
|
#### --release-tag RELEASE-TAG
|
||||||
|
|
||||||
|
Set release tags if the image deployment is successful. Multiple
|
||||||
|
arguments may be provided, alternating tag keys and values (see examples).
|
||||||
|
Hint: Empty values may be specified with "" (bash, cmd.exe) or '""' (PowerShell).
|
||||||
|
|
||||||
#### -e, --emulated
|
#### -e, --emulated
|
||||||
|
|
||||||
Use QEMU for ARM architecture emulation during the image build
|
Use QEMU for ARM architecture emulation during the image build
|
||||||
|
@ -20,22 +20,30 @@ import type { ImageDescriptor } from 'resin-compose-parse';
|
|||||||
|
|
||||||
import Command from '../command';
|
import Command from '../command';
|
||||||
import { ExpectedError } from '../errors';
|
import { ExpectedError } from '../errors';
|
||||||
import { getBalenaSdk, getChalk } from '../utils/lazy';
|
import { getBalenaSdk, getChalk, stripIndent } from '../utils/lazy';
|
||||||
import { dockerignoreHelp, registrySecretsHelp } from '../utils/messages';
|
import { dockerignoreHelp, registrySecretsHelp } from '../utils/messages';
|
||||||
import * as compose from '../utils/compose';
|
import * as compose from '../utils/compose';
|
||||||
import type {
|
import type {
|
||||||
BuiltImage,
|
BuiltImage,
|
||||||
ComposeCliFlags,
|
ComposeCliFlags,
|
||||||
ComposeOpts,
|
ComposeOpts,
|
||||||
|
Release as ComposeReleaseInfo,
|
||||||
} from '../utils/compose-types';
|
} from '../utils/compose-types';
|
||||||
import type { DockerCliFlags } from '../utils/docker';
|
import type { DockerCliFlags } from '../utils/docker';
|
||||||
import {
|
import {
|
||||||
|
applyReleaseTagKeysAndValues,
|
||||||
buildProject,
|
buildProject,
|
||||||
composeCliFlags,
|
composeCliFlags,
|
||||||
isBuildConfig,
|
isBuildConfig,
|
||||||
|
parseReleaseTagKeysAndValues,
|
||||||
} from '../utils/compose_ts';
|
} from '../utils/compose_ts';
|
||||||
import { dockerCliFlags } from '../utils/docker';
|
import { dockerCliFlags } from '../utils/docker';
|
||||||
import type { Application, ApplicationType, DeviceType } from 'balena-sdk';
|
import type {
|
||||||
|
Application,
|
||||||
|
ApplicationType,
|
||||||
|
DeviceType,
|
||||||
|
Release,
|
||||||
|
} from 'balena-sdk';
|
||||||
|
|
||||||
interface ApplicationWithArch extends Application {
|
interface ApplicationWithArch extends Application {
|
||||||
arch: string;
|
arch: string;
|
||||||
@ -45,6 +53,7 @@ interface FlagsDef extends ComposeCliFlags, DockerCliFlags {
|
|||||||
source?: string;
|
source?: string;
|
||||||
build: boolean;
|
build: boolean;
|
||||||
nologupload: boolean;
|
nologupload: boolean;
|
||||||
|
'release-tag'?: string[];
|
||||||
help: void;
|
help: void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -85,6 +94,7 @@ ${dockerignoreHelp}
|
|||||||
'$ balena deploy myApp',
|
'$ balena deploy myApp',
|
||||||
'$ balena deploy myApp --build --source myBuildDir/',
|
'$ balena deploy myApp --build --source myBuildDir/',
|
||||||
'$ balena deploy myApp myApp/myImage',
|
'$ balena deploy myApp myApp/myImage',
|
||||||
|
'$ balena deploy myApp myApp/myImage --release-tag key1 "" key2 "value2 with spaces"',
|
||||||
];
|
];
|
||||||
|
|
||||||
public static args = [
|
public static args = [
|
||||||
@ -115,6 +125,14 @@ ${dockerignoreHelp}
|
|||||||
description:
|
description:
|
||||||
"don't upload build logs to the dashboard with image (if building)",
|
"don't upload build logs to the dashboard with image (if building)",
|
||||||
}),
|
}),
|
||||||
|
'release-tag': flags.string({
|
||||||
|
description: stripIndent`
|
||||||
|
Set release tags if the image deployment is successful. Multiple
|
||||||
|
arguments may be provided, alternating tag keys and values (see examples).
|
||||||
|
Hint: Empty values may be specified with "" (bash, cmd.exe) or '""' (PowerShell).
|
||||||
|
`,
|
||||||
|
multiple: true,
|
||||||
|
}),
|
||||||
...composeCliFlags,
|
...composeCliFlags,
|
||||||
...dockerCliFlags,
|
...dockerCliFlags,
|
||||||
// NOTE: Not supporting -h for help, because of clash with -h in DockerCliFlags
|
// NOTE: Not supporting -h for help, because of clash with -h in DockerCliFlags
|
||||||
@ -151,6 +169,10 @@ ${dockerignoreHelp}
|
|||||||
'../utils/compose_ts'
|
'../utils/compose_ts'
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const { releaseTagKeys, releaseTagValues } = parseReleaseTagKeysAndValues(
|
||||||
|
options['release-tag'] ?? [],
|
||||||
|
);
|
||||||
|
|
||||||
if (image) {
|
if (image) {
|
||||||
options['registry-secrets'] = await getRegistrySecrets(
|
options['registry-secrets'] = await getRegistrySecrets(
|
||||||
sdk,
|
sdk,
|
||||||
@ -180,7 +202,7 @@ ${dockerignoreHelp}
|
|||||||
compose.generateOpts(options),
|
compose.generateOpts(options),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
await this.deployProject(docker, logger, composeOpts, {
|
const release = await this.deployProject(docker, logger, composeOpts, {
|
||||||
app,
|
app,
|
||||||
appName, // may be prefixed by 'owner/', unlike app.app_name
|
appName, // may be prefixed by 'owner/', unlike app.app_name
|
||||||
image,
|
image,
|
||||||
@ -189,6 +211,12 @@ ${dockerignoreHelp}
|
|||||||
buildEmulated: !!options.emulated,
|
buildEmulated: !!options.emulated,
|
||||||
buildOpts,
|
buildOpts,
|
||||||
});
|
});
|
||||||
|
await applyReleaseTagKeysAndValues(
|
||||||
|
sdk,
|
||||||
|
release.id,
|
||||||
|
releaseTagKeys,
|
||||||
|
releaseTagValues,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async deployProject(
|
async deployProject(
|
||||||
@ -286,7 +314,7 @@ ${dockerignoreHelp}
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
let release;
|
let release: Release | ComposeReleaseInfo['release'];
|
||||||
if (appType?.is_legacy) {
|
if (appType?.is_legacy) {
|
||||||
const { deployLegacy } = require('../utils/deploy-legacy');
|
const { deployLegacy } = require('../utils/deploy-legacy');
|
||||||
|
|
||||||
@ -344,6 +372,7 @@ ${dockerignoreHelp}
|
|||||||
console.log();
|
console.log();
|
||||||
console.log(doodles.getDoodle()); // Show charlie
|
console.log(doodles.getDoodle()); // Show charlie
|
||||||
console.log();
|
console.log();
|
||||||
|
return release;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.logError('Deploy failed');
|
logger.logError('Deploy failed');
|
||||||
throw err;
|
throw err;
|
||||||
|
@ -25,12 +25,20 @@ import { ExpectedError, instanceOf } from '../errors';
|
|||||||
import { isV13 } from '../utils/version';
|
import { isV13 } from '../utils/version';
|
||||||
import { RegistrySecrets } from 'resin-multibuild';
|
import { RegistrySecrets } from 'resin-multibuild';
|
||||||
import { lowercaseIfSlug } from '../utils/normalization';
|
import { lowercaseIfSlug } from '../utils/normalization';
|
||||||
|
import {
|
||||||
|
applyReleaseTagKeysAndValues,
|
||||||
|
parseReleaseTagKeysAndValues,
|
||||||
|
} from '../utils/compose_ts';
|
||||||
|
|
||||||
enum BuildTarget {
|
enum BuildTarget {
|
||||||
Cloud,
|
Cloud,
|
||||||
Device,
|
Device,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface ArgsDef {
|
||||||
|
applicationOrDevice: string;
|
||||||
|
}
|
||||||
|
|
||||||
interface FlagsDef {
|
interface FlagsDef {
|
||||||
source: string;
|
source: string;
|
||||||
emulated: boolean;
|
emulated: boolean;
|
||||||
@ -53,10 +61,6 @@ interface FlagsDef {
|
|||||||
help: void;
|
help: void;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ArgsDef {
|
|
||||||
applicationOrDevice: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class PushCmd extends Command {
|
export default class PushCmd extends Command {
|
||||||
public static description = stripIndent`
|
public static description = stripIndent`
|
||||||
Start a build on the remote balenaCloud build servers, or a local mode device.
|
Start a build on the remote balenaCloud build servers, or a local mode device.
|
||||||
@ -339,23 +343,9 @@ export default class PushCmd extends Command {
|
|||||||
'is only valid when pushing to a local mode device',
|
'is only valid when pushing to a local mode device',
|
||||||
);
|
);
|
||||||
|
|
||||||
const releaseTags = options['release-tag'] ?? [];
|
const { releaseTagKeys, releaseTagValues } = parseReleaseTagKeysAndValues(
|
||||||
const releaseTagKeys = releaseTags.filter((_v, i) => i % 2 === 0);
|
options['release-tag'] ?? [],
|
||||||
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('');
|
|
||||||
}
|
|
||||||
|
|
||||||
await Command.checkLoggedIn();
|
await Command.checkLoggedIn();
|
||||||
const [token, baseUrl] = await Promise.all([
|
const [token, baseUrl] = await Promise.all([
|
||||||
@ -395,14 +385,11 @@ export default class PushCmd extends Command {
|
|||||||
};
|
};
|
||||||
const releaseId = await remote.startRemoteBuild(args);
|
const releaseId = await remote.startRemoteBuild(args);
|
||||||
if (releaseId) {
|
if (releaseId) {
|
||||||
// Above we have checked that releaseTagKeys and releaseTagValues are of the same size
|
await applyReleaseTagKeysAndValues(
|
||||||
const _ = await import('lodash');
|
sdk,
|
||||||
await Promise.all(
|
releaseId,
|
||||||
(_.zip(releaseTagKeys, releaseTagValues) as Array<
|
releaseTagKeys,
|
||||||
[string, string]
|
releaseTagValues,
|
||||||
>).map(async ([key, value]) => {
|
|
||||||
await sdk.models.release.tags.set(releaseId, key, value);
|
|
||||||
}),
|
|
||||||
);
|
);
|
||||||
} else if (releaseTagKeys.length > 0) {
|
} else if (releaseTagKeys.length > 0) {
|
||||||
throw new Error(stripIndent`
|
throw new Error(stripIndent`
|
||||||
|
11
lib/utils/compose-types.d.ts
vendored
11
lib/utils/compose-types.d.ts
vendored
@ -81,7 +81,16 @@ export interface ComposeProject {
|
|||||||
|
|
||||||
export interface Release {
|
export interface Release {
|
||||||
client: ReturnType<typeof import('balena-release').createClient>;
|
client: ReturnType<typeof import('balena-release').createClient>;
|
||||||
release: Partial<import('balena-release/build/models').ReleaseModel>;
|
release: Pick<
|
||||||
|
import('balena-release/build/models').ReleaseModel,
|
||||||
|
| 'id'
|
||||||
|
| 'status'
|
||||||
|
| 'commit'
|
||||||
|
| 'composition'
|
||||||
|
| 'source'
|
||||||
|
| 'start_timestamp'
|
||||||
|
| 'end_timestamp'
|
||||||
|
>;
|
||||||
serviceImages: Partial<import('balena-release/build/models').ImageModel>;
|
serviceImages: Partial<import('balena-release/build/models').ImageModel>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -200,11 +200,14 @@ export const createRelease = async function (
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
client,
|
client,
|
||||||
release: _.omit(release, [
|
release: _.pick(release, [
|
||||||
'created_at',
|
'id',
|
||||||
'belongs_to__application',
|
'status',
|
||||||
'is_created_by__user',
|
'commit',
|
||||||
'__metadata',
|
'composition',
|
||||||
|
'source',
|
||||||
|
'start_timestamp',
|
||||||
|
'end_timestamp',
|
||||||
]),
|
]),
|
||||||
serviceImages: _.mapValues(serviceImages, (serviceImage) =>
|
serviceImages: _.mapValues(serviceImages, (serviceImage) =>
|
||||||
_.omit(serviceImage, [
|
_.omit(serviceImage, [
|
||||||
|
@ -44,6 +44,60 @@ import type { DeviceInfo } from './device/api';
|
|||||||
import { getBalenaSdk, getChalk, stripIndent } from './lazy';
|
import { getBalenaSdk, getChalk, stripIndent } from './lazy';
|
||||||
import Logger = require('./logger');
|
import Logger = require('./logger');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given an array representing the raw `--release-tag` flag of the deploy and
|
||||||
|
* push commands, parse it into separate arrays of release tag keys and values.
|
||||||
|
* The returned keys and values arrays are guaranteed to be of the same length.
|
||||||
|
*/
|
||||||
|
export function parseReleaseTagKeysAndValues(
|
||||||
|
releaseTags: string[],
|
||||||
|
): { releaseTagKeys: string[]; releaseTagValues: string[] } {
|
||||||
|
if (releaseTags.length === 0) {
|
||||||
|
return { releaseTagKeys: [], releaseTagValues: [] };
|
||||||
|
}
|
||||||
|
|
||||||
|
const releaseTagKeys = releaseTags.filter((_v, i) => i % 2 === 0);
|
||||||
|
const releaseTagValues = releaseTags.filter((_v, i) => i % 2 === 1);
|
||||||
|
|
||||||
|
releaseTagKeys.forEach((key: string) => {
|
||||||
|
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('');
|
||||||
|
}
|
||||||
|
return { releaseTagKeys, releaseTagValues };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Use the balena SDK `models.release.tags.set()` method to set release tags
|
||||||
|
* for the given release ID. The releaseTagKeys and releaseTagValues arrays
|
||||||
|
* must be of the same length; their items map 1-to-1 to form key-value pairs.
|
||||||
|
*/
|
||||||
|
export async function applyReleaseTagKeysAndValues(
|
||||||
|
sdk: BalenaSDK,
|
||||||
|
releaseId: number,
|
||||||
|
releaseTagKeys: string[],
|
||||||
|
releaseTagValues: string[],
|
||||||
|
) {
|
||||||
|
if (releaseTagKeys.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await Promise.all(
|
||||||
|
(_.zip(releaseTagKeys, releaseTagValues) as Array<[string, string]>).map(
|
||||||
|
async ([key, value]) => {
|
||||||
|
await sdk.models.release.tags.set(releaseId, key, value);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const exists = async (filename: string) => {
|
const exists = async (filename: string) => {
|
||||||
try {
|
try {
|
||||||
await fs.access(filename);
|
await fs.access(filename);
|
||||||
@ -1199,7 +1253,7 @@ export async function deployProject(
|
|||||||
auth: string,
|
auth: string,
|
||||||
apiEndpoint: string,
|
apiEndpoint: string,
|
||||||
skipLogUpload: boolean,
|
skipLogUpload: boolean,
|
||||||
): Promise<Partial<import('balena-release/build/models').ReleaseModel>> {
|
): Promise<import('balena-release/build/models').ReleaseModel> {
|
||||||
const releaseMod = await import('balena-release');
|
const releaseMod = await import('balena-release');
|
||||||
const { createRelease, tagServiceImages } = await import('./compose');
|
const { createRelease, tagServiceImages } = await import('./compose');
|
||||||
const tty = (await import('./tty'))(process.stdout);
|
const tty = (await import('./tty'))(process.stdout);
|
||||||
|
Loading…
Reference in New Issue
Block a user