Merge pull request #2154 from balena-io/add_release_tags_to_deploy

Add release-tag on deploy command
This commit is contained in:
bulldozer-balena[bot] 2021-01-14 23:24:51 +00:00 committed by GitHub
commit 743de66138
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 129 additions and 40 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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