1 | "use strict";
|
2 | Object.defineProperty(exports, "__esModule", { value: true });
|
3 | const cli_framework_1 = require("@ionic/cli-framework");
|
4 | const format_1 = require("@ionic/cli-framework/utils/format");
|
5 | const utils_fs_1 = require("@ionic/utils-fs");
|
6 | const utils_process_1 = require("@ionic/utils-process");
|
7 | const chalk_1 = require("chalk");
|
8 | const Debug = require("debug");
|
9 | const fs = require("fs");
|
10 | const guards_1 = require("../../guards");
|
11 | const color_1 = require("../../lib/color");
|
12 | const command_1 = require("../../lib/command");
|
13 | const errors_1 = require("../../lib/errors");
|
14 | const file_1 = require("../../lib/utils/file");
|
15 | const http_1 = require("../../lib/utils/http");
|
16 | const debug = Debug('ionic:commands:package:build');
|
17 | const PLATFORMS = ['android', 'ios'];
|
18 | const ANDROID_BUILD_TYPES = ['debug', 'release'];
|
19 | const IOS_BUILD_TYPES = ['development', 'ad-hoc', 'app-store', 'enterprise'];
|
20 | const BUILD_TYPES = ANDROID_BUILD_TYPES.concat(IOS_BUILD_TYPES);
|
21 | const TARGET_PLATFORM = ['Android', 'iOS - Xcode 10 (Preferred)', 'iOS - Xcode 9', 'iOS - Xcode 8'];
|
22 | class 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: `
|
30 | This 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 |
|
32 | Apart from ${color_1.input('--commit')}, every option can be specified using the full name setup within the Dashboard[^dashboard].
|
33 |
|
34 | The ${color_1.input('--security-profile')} option is mandatory for any iOS build but not for Android debug builds.
|
35 |
|
36 | Customizing 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" ],
|
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" ],
|
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" ],
|
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 |
|
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 |
|
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 | }
|
300 | exports.BuildCommand = BuildCommand;
|