UNPKG

14.3 kBJavaScriptView Raw
1"use strict";
2Object.defineProperty(exports, "__esModule", { value: true });
3const cli_framework_1 = require("@ionic/cli-framework");
4const format_1 = require("@ionic/cli-framework/utils/format");
5const utils_fs_1 = require("@ionic/utils-fs");
6const utils_process_1 = require("@ionic/utils-process");
7const chalk_1 = require("chalk");
8const Debug = require("debug");
9const fs = require("fs");
10const guards_1 = require("../../guards");
11const color_1 = require("../../lib/color");
12const command_1 = require("../../lib/command");
13const errors_1 = require("../../lib/errors");
14const file_1 = require("../../lib/utils/file");
15const http_1 = require("../../lib/utils/http");
16const debug = Debug('ionic:commands:package:build');
17const PLATFORMS = ['android', 'ios'];
18const ANDROID_BUILD_TYPES = ['debug', 'release'];
19const IOS_BUILD_TYPES = ['development', 'ad-hoc', 'app-store', 'enterprise'];
20const BUILD_TYPES = ANDROID_BUILD_TYPES.concat(IOS_BUILD_TYPES);
21const TARGET_PLATFORM = ['Android', 'iOS - Xcode 10 (Preferred)', 'iOS - Xcode 9', 'iOS - Xcode 8'];
22class BuildCommand extends command_1.Command {
23 async getMetadata() {
24 const dashUrl = this.env.config.getDashUrl();
25 return {
26 name: 'build',
27 type: 'project',
28 summary: 'Create a package build on Appflow',
29 description: `
30This command creates a package build on Ionic Appflow. While the build is running, it prints the remote build log to the terminal. If the build is successful, it downloads the created app package file in the current directory.
31
32Apart from ${color_1.input('--commit')}, every option can be specified using the full name setup within the Dashboard[^dashboard].
33
34The ${color_1.input('--security-profile')} option is mandatory for any iOS build but not for Android debug builds.
35
36Customizing the build:
37- The ${color_1.input('--environment')} and ${color_1.input('--native-config')} options can be used to customize the groups of values exposed to the build.
38- Override the preferred platform with ${color_1.input('--target-platform')}. This is useful for building older iOS apps.
39`,
40 footnotes: [
41 {
42 id: 'dashboard',
43 url: dashUrl,
44 },
45 ],
46 exampleCommands: [
47 'android debug',
48 'ios development --security-profile="iOS Security Profile Name"',
49 'android debug --environment="My Custom Environment Name"',
50 'android debug --native-config="My Custom Native Config Name"',
51 'android debug --commit=2345cd3305a1cf94de34e93b73a932f25baac77c',
52 'ios development --security-profile="iOS Security Profile Name" --target-platform="iOS - Xcode 9"',
53 'ios development --security-profile="iOS Security Profile Name" --build-file-name=my_custom_file_name.ipa',
54 ],
55 inputs: [
56 {
57 name: 'platform',
58 summary: `The platform to package (${PLATFORMS.map(v => color_1.input(v)).join(', ')})`,
59 validators: [cli_framework_1.validators.required, cli_framework_1.contains(PLATFORMS, {})],
60 },
61 {
62 name: 'type',
63 summary: `The build type (${BUILD_TYPES.map(v => color_1.input(v)).join(', ')})`,
64 validators: [cli_framework_1.validators.required, cli_framework_1.contains(BUILD_TYPES, {})],
65 },
66 ],
67 options: [
68 {
69 name: 'security-profile',
70 summary: 'Security profile',
71 type: String,
72 spec: { value: 'name' },
73 },
74 {
75 name: 'environment',
76 summary: 'The group of environment variables exposed to your build',
77 type: String,
78 spec: { value: 'name' },
79 },
80 {
81 name: 'native-config',
82 summary: 'The group of native config variables exposed to your build',
83 type: String,
84 spec: { value: 'name' },
85 },
86 {
87 name: 'commit',
88 summary: 'Commit (defaults to HEAD)',
89 type: String,
90 groups: ["advanced" /* ADVANCED */],
91 spec: { value: 'sha1' },
92 },
93 {
94 name: 'target-platform',
95 summary: `Target platform (${TARGET_PLATFORM.map(v => color_1.input(`"${v}"`)).join(', ')})`,
96 type: String,
97 groups: ["advanced" /* ADVANCED */],
98 spec: { value: 'name' },
99 },
100 {
101 name: 'build-file-name',
102 summary: 'The name for the downloaded build file',
103 type: String,
104 groups: ["advanced" /* ADVANCED */],
105 spec: { value: 'name' },
106 },
107 ],
108 };
109 }
110 async preRun(inputs, options) {
111 if (!inputs[0]) {
112 const platformInput = await this.env.prompt({
113 type: 'list',
114 name: 'platform',
115 choices: PLATFORMS,
116 message: `Platform to package:`,
117 validate: v => cli_framework_1.combine(cli_framework_1.validators.required, cli_framework_1.contains(PLATFORMS, {}))(v),
118 });
119 inputs[0] = platformInput;
120 }
121 const buildTypes = inputs[0] === 'ios' ? IOS_BUILD_TYPES : ANDROID_BUILD_TYPES;
122 // validate that the build type is valid for the platform
123 let reenterBuilType = false;
124 if (inputs[1] && !buildTypes.includes(inputs[1])) {
125 reenterBuilType = true;
126 this.env.log.nl();
127 this.env.log.warn(`Build type ${color_1.strong(inputs[1])} incompatible for ${color_1.strong(inputs[0])}; please choose a correct one`);
128 }
129 if (!inputs[1] || reenterBuilType) {
130 const typeInput = await this.env.prompt({
131 type: 'list',
132 name: 'type',
133 choices: buildTypes,
134 message: `Build type:`,
135 validate: v => cli_framework_1.combine(cli_framework_1.validators.required, cli_framework_1.contains(buildTypes, {}))(v),
136 });
137 inputs[1] = typeInput;
138 }
139 // the security profile is mandatory for iOS packages, so prompting if it is missing
140 if (inputs[0] === 'ios' && !options['security-profile']) {
141 if (this.env.flags.interactive) {
142 this.env.log.nl();
143 this.env.log.warn(`A security profile is mandatory to build an iOS package`);
144 }
145 const securityProfileOption = await this.env.prompt({
146 type: 'input',
147 name: 'security-profile',
148 message: `Security Profile Name:`,
149 });
150 options['security-profile'] = securityProfileOption;
151 }
152 }
153 async run(inputs, options) {
154 if (!this.project) {
155 throw new errors_1.FatalException(`Cannot run ${color_1.input('ionic package build')} outside a project directory.`);
156 }
157 const token = this.env.session.getUserToken();
158 const appflowId = await this.project.requireAppflowId();
159 const [platform, buildType] = inputs;
160 if (!options.commit) {
161 options.commit = (await this.env.shell.output('git', ['rev-parse', 'HEAD'], { cwd: this.project.directory })).trim();
162 debug(`Commit hash: ${color_1.strong(options.commit)}`);
163 }
164 let build = await this.createPackageBuild(appflowId, token, platform, buildType, options);
165 const buildId = build.job_id;
166 let customBuildFileName = '';
167 if (options['build-file-name']) {
168 if (typeof (options['build-file-name']) !== 'string' || !file_1.fileUtils.isValidFileName(options['build-file-name'])) {
169 throw new errors_1.FatalException(`${color_1.strong(String(options['build-file-name']))} is not a valid file name`);
170 }
171 customBuildFileName = String(options['build-file-name']);
172 }
173 const details = format_1.columnar([
174 ['App ID', color_1.strong(appflowId)],
175 ['Build ID', color_1.strong(buildId.toString())],
176 ['Commit', color_1.strong(`${build.commit.sha.substring(0, 6)} ${build.commit.note}`)],
177 ['Target Platform', color_1.strong(build.stack.friendly_name)],
178 ['Build Type', color_1.strong(build.build_type)],
179 ['Security Profile', build.profile_tag ? color_1.strong(build.profile_tag) : color_1.weak('not set')],
180 ['Environment', build.environment_name ? color_1.strong(build.environment_name) : color_1.weak('not set')],
181 ['Native Config', build.native_config_name ? color_1.strong(build.native_config_name) : color_1.weak('not set')],
182 ], { vsep: ':' });
183 this.env.log.ok(`Build created\n` +
184 details + '\n\n');
185 build = await this.tailBuildLog(appflowId, buildId, token);
186 if (build.state !== 'success') {
187 throw new Error('Build failed');
188 }
189 const url = await this.getDownloadUrl(appflowId, buildId, token);
190 if (!url.url) {
191 throw new Error('Missing URL in response');
192 }
193 const filename = await this.downloadBuild(url.url, customBuildFileName);
194 this.env.log.ok(`Build completed: ${filename}`);
195 }
196 async createPackageBuild(appflowId, token, platform, buildType, options) {
197 const { req } = await this.env.client.make('POST', `/apps/${appflowId}/packages/verbose_post`);
198 req.set('Authorization', `Bearer ${token}`).send({
199 platform,
200 build_type: buildType,
201 commit_sha: options.commit,
202 stack_name: options['target-platform'],
203 profile_name: options['security-profile'],
204 environment_name: options.environment,
205 native_config_name: options['native-config'],
206 });
207 try {
208 const res = await this.env.client.do(req);
209 return res.data;
210 }
211 catch (e) {
212 if (guards_1.isSuperAgentError(e)) {
213 if (e.response.status === 401) {
214 this.env.log.error('Try logging out and back in again.');
215 }
216 const apiErrorMessage = (e.response.body.error && e.response.body.error.message) ? e.response.body.error.message : 'Api Error';
217 throw new errors_1.FatalException(`Unable to create build: ` + apiErrorMessage);
218 }
219 else {
220 throw e;
221 }
222 }
223 }
224 async getPackageBuild(appflowId, buildId, token) {
225 const { req } = await this.env.client.make('GET', `/apps/${appflowId}/packages/${buildId}`);
226 req.set('Authorization', `Bearer ${token}`).send();
227 try {
228 const res = await this.env.client.do(req);
229 return res.data;
230 }
231 catch (e) {
232 if (guards_1.isSuperAgentError(e)) {
233 if (e.response.status === 401) {
234 this.env.log.error('Try logging out and back in again.');
235 }
236 const apiErrorMessage = (e.response.body.error && e.response.body.error.message) ? e.response.body.error.message : 'Api Error';
237 throw new errors_1.FatalException(`Unable to get build ${buildId}: ` + apiErrorMessage);
238 }
239 else {
240 throw e;
241 }
242 }
243 }
244 async getDownloadUrl(appflowId, buildId, token) {
245 const { req } = await this.env.client.make('GET', `/apps/${appflowId}/packages/${buildId}/download`);
246 req.set('Authorization', `Bearer ${token}`).send();
247 try {
248 const res = await this.env.client.do(req);
249 return res.data;
250 }
251 catch (e) {
252 if (guards_1.isSuperAgentError(e)) {
253 if (e.response.status === 401) {
254 this.env.log.error('Try logging out and back in again.');
255 }
256 const apiErrorMessage = (e.response.body.error && e.response.body.error.message) ? e.response.body.error.message : 'Api Error';
257 throw new errors_1.FatalException(`Unable to get download URL for build ${buildId}: ` + apiErrorMessage);
258 }
259 else {
260 throw e;
261 }
262 }
263 }
264 async tailBuildLog(appflowId, buildId, token) {
265 let build;
266 let start = 0;
267 const ws = this.env.log.createWriteStream(cli_framework_1.LOGGER_LEVELS.INFO, false);
268 let isCreatedMessage = false;
269 while (!(build && (build.state === 'success' || build.state === 'failed'))) {
270 await utils_process_1.sleep(5000);
271 build = await this.getPackageBuild(appflowId, buildId, token);
272 if (build && build.state === 'created' && !isCreatedMessage) {
273 ws.write(chalk_1.default.yellow('Concurrency limit reached: build will start as soon as other builds finish.'));
274 isCreatedMessage = true;
275 }
276 const trace = build.job.trace;
277 if (trace.length > start) {
278 ws.write(trace.substring(start));
279 start = trace.length;
280 }
281 }
282 ws.end();
283 return build;
284 }
285 async downloadBuild(url, filename) {
286 const { req } = await http_1.createRequest('GET', url, this.env.config.getHTTPConfig());
287 if (!filename) {
288 req.on('response', res => {
289 const contentDisposition = res.header['content-disposition'];
290 filename = contentDisposition ? contentDisposition.split('=')[1] : 'output.bin';
291 });
292 }
293 const tmpFile = utils_fs_1.tmpfilepath('ionic-package-build');
294 const ws = fs.createWriteStream(tmpFile);
295 await http_1.download(req, ws, {});
296 fs.renameSync(tmpFile, filename);
297 return filename;
298 }
299}
300exports.BuildCommand = BuildCommand;