mirror of
https://github.com/balena-io/balena-cli.git
synced 2024-12-22 15:02: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 --build --source myBuildDir/
|
||||
$ balena deploy myApp myApp/myImage
|
||||
$ balena deploy myApp myApp/myImage --release-tag key1 "" key2 "value2 with spaces"
|
||||
|
||||
### Arguments
|
||||
|
||||
@ -3077,6 +3078,12 @@ force a rebuild before deploy
|
||||
|
||||
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
|
||||
|
||||
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 { ExpectedError } from '../errors';
|
||||
import { getBalenaSdk, getChalk } from '../utils/lazy';
|
||||
import { getBalenaSdk, getChalk, stripIndent } from '../utils/lazy';
|
||||
import { dockerignoreHelp, registrySecretsHelp } from '../utils/messages';
|
||||
import * as compose from '../utils/compose';
|
||||
import type {
|
||||
BuiltImage,
|
||||
ComposeCliFlags,
|
||||
ComposeOpts,
|
||||
Release as ComposeReleaseInfo,
|
||||
} from '../utils/compose-types';
|
||||
import type { DockerCliFlags } from '../utils/docker';
|
||||
import {
|
||||
applyReleaseTagKeysAndValues,
|
||||
buildProject,
|
||||
composeCliFlags,
|
||||
isBuildConfig,
|
||||
parseReleaseTagKeysAndValues,
|
||||
} from '../utils/compose_ts';
|
||||
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 {
|
||||
arch: string;
|
||||
@ -45,6 +53,7 @@ interface FlagsDef extends ComposeCliFlags, DockerCliFlags {
|
||||
source?: string;
|
||||
build: boolean;
|
||||
nologupload: boolean;
|
||||
'release-tag'?: string[];
|
||||
help: void;
|
||||
}
|
||||
|
||||
@ -85,6 +94,7 @@ ${dockerignoreHelp}
|
||||
'$ balena deploy myApp',
|
||||
'$ balena deploy myApp --build --source myBuildDir/',
|
||||
'$ balena deploy myApp myApp/myImage',
|
||||
'$ balena deploy myApp myApp/myImage --release-tag key1 "" key2 "value2 with spaces"',
|
||||
];
|
||||
|
||||
public static args = [
|
||||
@ -115,6 +125,14 @@ ${dockerignoreHelp}
|
||||
description:
|
||||
"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,
|
||||
...dockerCliFlags,
|
||||
// NOTE: Not supporting -h for help, because of clash with -h in DockerCliFlags
|
||||
@ -151,6 +169,10 @@ ${dockerignoreHelp}
|
||||
'../utils/compose_ts'
|
||||
);
|
||||
|
||||
const { releaseTagKeys, releaseTagValues } = parseReleaseTagKeysAndValues(
|
||||
options['release-tag'] ?? [],
|
||||
);
|
||||
|
||||
if (image) {
|
||||
options['registry-secrets'] = await getRegistrySecrets(
|
||||
sdk,
|
||||
@ -180,7 +202,7 @@ ${dockerignoreHelp}
|
||||
compose.generateOpts(options),
|
||||
]);
|
||||
|
||||
await this.deployProject(docker, logger, composeOpts, {
|
||||
const release = await this.deployProject(docker, logger, composeOpts, {
|
||||
app,
|
||||
appName, // may be prefixed by 'owner/', unlike app.app_name
|
||||
image,
|
||||
@ -189,6 +211,12 @@ ${dockerignoreHelp}
|
||||
buildEmulated: !!options.emulated,
|
||||
buildOpts,
|
||||
});
|
||||
await applyReleaseTagKeysAndValues(
|
||||
sdk,
|
||||
release.id,
|
||||
releaseTagKeys,
|
||||
releaseTagValues,
|
||||
);
|
||||
}
|
||||
|
||||
async deployProject(
|
||||
@ -286,7 +314,7 @@ ${dockerignoreHelp}
|
||||
},
|
||||
);
|
||||
|
||||
let release;
|
||||
let release: Release | ComposeReleaseInfo['release'];
|
||||
if (appType?.is_legacy) {
|
||||
const { deployLegacy } = require('../utils/deploy-legacy');
|
||||
|
||||
@ -344,6 +372,7 @@ ${dockerignoreHelp}
|
||||
console.log();
|
||||
console.log(doodles.getDoodle()); // Show charlie
|
||||
console.log();
|
||||
return release;
|
||||
} catch (err) {
|
||||
logger.logError('Deploy failed');
|
||||
throw err;
|
||||
|
@ -25,12 +25,20 @@ import { ExpectedError, instanceOf } from '../errors';
|
||||
import { isV13 } from '../utils/version';
|
||||
import { RegistrySecrets } from 'resin-multibuild';
|
||||
import { lowercaseIfSlug } from '../utils/normalization';
|
||||
import {
|
||||
applyReleaseTagKeysAndValues,
|
||||
parseReleaseTagKeysAndValues,
|
||||
} from '../utils/compose_ts';
|
||||
|
||||
enum BuildTarget {
|
||||
Cloud,
|
||||
Device,
|
||||
}
|
||||
|
||||
interface ArgsDef {
|
||||
applicationOrDevice: string;
|
||||
}
|
||||
|
||||
interface FlagsDef {
|
||||
source: string;
|
||||
emulated: boolean;
|
||||
@ -53,10 +61,6 @@ interface FlagsDef {
|
||||
help: void;
|
||||
}
|
||||
|
||||
interface ArgsDef {
|
||||
applicationOrDevice: string;
|
||||
}
|
||||
|
||||
export default class PushCmd extends Command {
|
||||
public static description = stripIndent`
|
||||
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',
|
||||
);
|
||||
|
||||
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 { releaseTagKeys, releaseTagValues } = parseReleaseTagKeysAndValues(
|
||||
options['release-tag'] ?? [],
|
||||
);
|
||||
|
||||
await Command.checkLoggedIn();
|
||||
const [token, baseUrl] = await Promise.all([
|
||||
@ -395,14 +385,11 @@ export default class PushCmd extends Command {
|
||||
};
|
||||
const releaseId = await remote.startRemoteBuild(args);
|
||||
if (releaseId) {
|
||||
// Above we have checked that releaseTagKeys and releaseTagValues are of the same size
|
||||
const _ = await import('lodash');
|
||||
await Promise.all(
|
||||
(_.zip(releaseTagKeys, releaseTagValues) as Array<
|
||||
[string, string]
|
||||
>).map(async ([key, value]) => {
|
||||
await sdk.models.release.tags.set(releaseId, key, value);
|
||||
}),
|
||||
await applyReleaseTagKeysAndValues(
|
||||
sdk,
|
||||
releaseId,
|
||||
releaseTagKeys,
|
||||
releaseTagValues,
|
||||
);
|
||||
} else if (releaseTagKeys.length > 0) {
|
||||
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 {
|
||||
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>;
|
||||
}
|
||||
|
||||
|
@ -200,11 +200,14 @@ export const createRelease = async function (
|
||||
|
||||
return {
|
||||
client,
|
||||
release: _.omit(release, [
|
||||
'created_at',
|
||||
'belongs_to__application',
|
||||
'is_created_by__user',
|
||||
'__metadata',
|
||||
release: _.pick(release, [
|
||||
'id',
|
||||
'status',
|
||||
'commit',
|
||||
'composition',
|
||||
'source',
|
||||
'start_timestamp',
|
||||
'end_timestamp',
|
||||
]),
|
||||
serviceImages: _.mapValues(serviceImages, (serviceImage) =>
|
||||
_.omit(serviceImage, [
|
||||
|
@ -44,6 +44,60 @@ import type { DeviceInfo } from './device/api';
|
||||
import { getBalenaSdk, getChalk, stripIndent } from './lazy';
|
||||
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) => {
|
||||
try {
|
||||
await fs.access(filename);
|
||||
@ -1199,7 +1253,7 @@ export async function deployProject(
|
||||
auth: string,
|
||||
apiEndpoint: string,
|
||||
skipLogUpload: boolean,
|
||||
): Promise<Partial<import('balena-release/build/models').ReleaseModel>> {
|
||||
): Promise<import('balena-release/build/models').ReleaseModel> {
|
||||
const releaseMod = await import('balena-release');
|
||||
const { createRelease, tagServiceImages } = await import('./compose');
|
||||
const tty = (await import('./tty'))(process.stdout);
|
||||
|
Loading…
Reference in New Issue
Block a user