Skip to content

Commit 2a88e33

Browse files
author
Mitko-Kerezov
committed
Implement publish command for iOS
Publishes to itunesconnect via Xcode's iTMS Transporter tool. Introduce itmstransporter-service to talk to the tool along with minor refactorings in the build process, allowing for custom setting of CODE_SIGN_IDENTITY and PROVISIONING_PROFILE during build.
1 parent 91ce213 commit 2a88e33

11 files changed

+439
-3
lines changed

lib/bootstrap.ts

+5
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,11 @@ $injector.requireCommand("test|ios", "./commands/test");
5252
$injector.requireCommand("test|init", "./commands/test-init");
5353
$injector.requireCommand("dev-generate-help", "./commands/generate-help");
5454

55+
$injector.requireCommand("appstore|*list", "./commands/appstore-list");
56+
$injector.requireCommand("appstore|upload", "./commands/appstore-upload");
57+
$injector.requireCommand("publish|ios", "./commands/appstore-upload");
58+
$injector.require("itmsTransporterService", "./services/itmstransporter-service");
59+
5560
$injector.require("npm", "./node-package-manager");
5661
$injector.require("npmInstallationManager", "./npm-installation-manager");
5762
$injector.require("lockfile", "./lockfile");

lib/commands/appstore-list.ts

+44
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
///<reference path="../.d.ts"/>
2+
"use strict";
3+
4+
import { createTable } from "../common/helpers";
5+
import {StringCommandParameter} from "../common/command-params";
6+
7+
export class ListiOSApps implements ICommand {
8+
constructor(private $injector: IInjector,
9+
private $itmsTransporterService: IITMSTransporterService,
10+
private $logger: ILogger,
11+
private $prompter: IPrompter,
12+
private $stringParameterBuilder: IStringParameterBuilder) { }
13+
14+
public allowedParameters: ICommandParameter[] = [new StringCommandParameter(this.$injector), new StringCommandParameter(this.$injector)];
15+
16+
public execute(args: string[]): IFuture<void> {
17+
return (() => {
18+
let username = args[0],
19+
password = args[1];
20+
21+
if(!username) {
22+
username = this.$prompter.getString("Apple ID", { allowEmpty: false }).wait();
23+
}
24+
25+
if(!password) {
26+
password = this.$prompter.getPassword("Apple ID password").wait();
27+
}
28+
29+
let iOSApplications = this.$itmsTransporterService.getiOSApplications({username, password}).wait();
30+
31+
if (!iOSApplications || !iOSApplications.length) {
32+
this.$logger.out("Seems you don't have any applications yet.");
33+
} else {
34+
let table: any = createTable(["Application Name", "Bundle Identifier", "Version"], iOSApplications.map(element => {
35+
return [element.name, element.bundleId, element.version];
36+
}));
37+
38+
this.$logger.out(table.toString());
39+
}
40+
}).future<void>()();
41+
}
42+
}
43+
44+
$injector.registerCommand("appstore|*list", ListiOSApps);

lib/commands/appstore-upload.ts

+67
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
///<reference path="../.d.ts"/>
2+
"use strict";
3+
4+
import {StringCommandParameter} from "../common/command-params";
5+
import * as path from "path";
6+
7+
export class PublishIOS implements ICommand {
8+
constructor(private $errors: IErrors,
9+
private $fs: IFileSystem,
10+
private $hostInfo: IHostInfo,
11+
private $injector: IInjector,
12+
private $itmsTransporterService: IITMSTransporterService,
13+
private $logger: ILogger,
14+
private $options: IOptions,
15+
private $prompter: IPrompter,
16+
private $stringParameterBuilder: IStringParameterBuilder) { }
17+
18+
public allowedParameters: ICommandParameter[] = [new StringCommandParameter(this.$injector), new StringCommandParameter(this.$injector),
19+
new StringCommandParameter(this.$injector), new StringCommandParameter(this.$injector)];
20+
21+
public execute(args: string[]): IFuture<void> {
22+
return (() => {
23+
let username = args[0],
24+
password = args[1],
25+
mobileProvisionIdentifier = args[2],
26+
codeSignIdentity = args[3],
27+
ipaFilePath = this.$options.ipa ? path.resolve(this.$options.ipa) : null;
28+
29+
if(!username) {
30+
username = this.$prompter.getString("Apple ID", { allowEmpty: false }).wait();
31+
}
32+
33+
if(!password) {
34+
password = this.$prompter.getPassword("Apple ID password").wait();
35+
}
36+
37+
if(!mobileProvisionIdentifier && !ipaFilePath) {
38+
this.$logger.warn("No mobile provision identifier set. A default mobile provision will be used. You can set one in app/App_Resources/iOS/build.xcconfig");
39+
}
40+
41+
if(!codeSignIdentity && !ipaFilePath) {
42+
this.$logger.warn("No code sign identity set. A default code sign identity will be used. You can set one in app/App_Resources/iOS/build.xcconfig");
43+
}
44+
45+
this.$options.release = true;
46+
this.$itmsTransporterService.upload({
47+
username,
48+
password,
49+
mobileProvisionIdentifier,
50+
codeSignIdentity,
51+
ipaFilePath,
52+
verboseLogging: this.$logger.getLevel() === "TRACE"
53+
}).wait();
54+
}).future<void>()();
55+
}
56+
57+
public canExecute(args: string[]): IFuture<boolean> {
58+
return (() => {
59+
if (!this.$hostInfo.isDarwin) {
60+
this.$errors.failWithoutHelp("This command is only available on Mac OS X.");
61+
}
62+
63+
return true;
64+
}).future<boolean>()();
65+
}
66+
}
67+
$injector.registerCommand(["publish|ios", "appstore|upload"], PublishIOS);

lib/constants.ts

+17
Original file line numberDiff line numberDiff line change
@@ -25,3 +25,20 @@ export class ReleaseType {
2525
static PREPATCH = "prepatch";
2626
static PRERELEASE = "prerelease";
2727
}
28+
29+
export class ITMSConstants {
30+
static ApplicationMetadataFile = "metadata.xml";
31+
static VerboseLoggingLevels = {
32+
Informational: "informational",
33+
Verbose: "detailed"
34+
};
35+
static iTMSExecutableName = "iTMSTransporter";
36+
static iTMSDirectoryName = "itms";
37+
}
38+
39+
class ItunesConnectApplicationTypesClass implements IiTunesConnectApplicationType {
40+
public iOS = "iOS App";
41+
public Mac = "Mac OS X App";
42+
}
43+
44+
export let ItunesConnectApplicationTypes = new ItunesConnectApplicationTypesClass();

lib/declarations.ts

+53
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ interface ILiveSyncService {
5858
}
5959

6060
interface IOptions extends ICommonOptions {
61+
ipa: string;
6162
frameworkPath: string;
6263
frameworkName: string;
6364
framework: string;
@@ -88,6 +89,58 @@ interface IInitService {
8889
initialize(): IFuture<void>;
8990
}
9091

92+
/**
93+
* Describes standard username/password type credentials.
94+
*/
95+
interface ICredentials {
96+
username: string;
97+
password: string;
98+
}
99+
100+
/**
101+
* Describes properties needed for uploading a package to iTunes Connect
102+
*/
103+
interface IITMSData extends ICredentials {
104+
/**
105+
* The identifier of the mobile provision used for building. Note that this will override the same option set through .xcconfig files.
106+
* @type {string}
107+
*/
108+
mobileProvisionIdentifier?: string;
109+
/**
110+
* The Code Sign Identity used for building. Note that this will override the same option set through .xcconfig files.
111+
* @type {string}
112+
*/
113+
codeSignIdentity?: string;
114+
/**
115+
* Path to a .ipa file which will be uploaded. If set that .ipa will be used and no build will be issued.
116+
* @type {string}
117+
*/
118+
ipaFilePath?: string;
119+
/**
120+
* Specifies whether the logging level of the itmstransporter command-line tool should be set to verbose.
121+
* @type {string}
122+
*/
123+
verboseLogging?: boolean;
124+
}
125+
126+
/**
127+
* Used for communicating with Xcode's iTMS Transporter tool.
128+
*/
129+
interface IITMSTransporterService {
130+
/**
131+
* Uploads an .ipa package to iTunes Connect.
132+
* @param {IITMSData} data Data needed to upload the package
133+
* @return {IFuture<void>}
134+
*/
135+
upload(data: IITMSData): IFuture<void>;
136+
/**
137+
* Queries Apple's content delivery API to get the user's registered iOS applications.
138+
* @param {ICredentials} credentials Credentials for authentication with iTunes Connect.
139+
* @return {IFuture<IItunesConnectApplication[]>} The user's iOS applications.
140+
*/
141+
getiOSApplications(credentials: ICredentials): IFuture<IiTunesConnectApplication[]>;
142+
}
143+
91144
/**
92145
* Provides access to information about installed Android tools and SDKs versions.
93146
*/

lib/definitions/project.d.ts

+14
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,20 @@ interface IBuildConfig {
5252
architectures?: string[];
5353
}
5454

55+
/**
56+
* Describes iOS-specific build configuration properties
57+
*/
58+
interface IiOSBuildConfig extends IBuildConfig {
59+
/**
60+
* Identifier of the mobile provision which will be used for the build. If not set a provision will be selected automatically if possible.
61+
*/
62+
mobileProvisionIdentifier?: string;
63+
/**
64+
* Code sign identity used for build. If not set iPhone Developer is used as a default when building for device.
65+
*/
66+
codeSignIdentity?: string;
67+
}
68+
5569
interface IPlatformProjectService {
5670
platformData: IPlatformData;
5771
validate(): IFuture<void>;

lib/options.ts

+1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ export class Options extends commonOptionsLibPath.OptionsBase {
1212
$staticConfig: IStaticConfig,
1313
$hostInfo: IHostInfo) {
1414
super({
15+
ipa: { type: OptionType.String },
1516
frameworkPath: { type: OptionType.String },
1617
frameworkName: { type: OptionType.String },
1718
framework: { type: OptionType.String },

lib/services/init-service.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ export class InitService implements IInitService {
9494
return defaultAppId;
9595
}
9696

97-
return this.$prompter.getString("Id:", () => defaultAppId).wait();
97+
return this.$prompter.getString("Id:", { defaultAction: () => defaultAppId }).wait();
9898
}).future<string>()();
9999
}
100100

lib/services/ios-project-service.ts

+9-1
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,7 @@ export class IOSProjectService extends projectServiceBaseLib.PlatformProjectServ
157157
}).future<void>()();
158158
}
159159

160-
public buildProject(projectRoot: string, buildConfig?: IBuildConfig): IFuture<void> {
160+
public buildProject(projectRoot: string, buildConfig?: IiOSBuildConfig): IFuture<void> {
161161
return (() => {
162162
let basicArgs = [
163163
"-configuration", this.$options.release ? "Release" : "Debug",
@@ -204,6 +204,14 @@ export class IOSProjectService extends projectServiceBaseLib.PlatformProjectServ
204204
]);
205205
}
206206

207+
if (buildConfig && buildConfig.codeSignIdentity) {
208+
args.push(`CODE_SIGN_IDENTITY=${buildConfig.codeSignIdentity}`);
209+
}
210+
211+
if (buildConfig && buildConfig.mobileProvisionIdentifier) {
212+
args.push(`PROVISIONING_PROFILE=${buildConfig.mobileProvisionIdentifier}`);
213+
}
214+
207215
this.$childProcess.spawnFromEvent("xcodebuild", args, "exit", {cwd: this.$options, stdio: 'inherit'}).wait();
208216

209217
if (buildForDevice) {

0 commit comments

Comments
 (0)