Skip to content

Commit dc14b32

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 dc14b32

11 files changed

+448
-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

+71
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
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+
bundleId = this.$itmsTransporterService.getBundleIdentifier(ipaFilePath).wait();
29+
30+
if(!username) {
31+
username = this.$prompter.getString("Apple ID", { allowEmpty: false }).wait();
32+
}
33+
34+
if(!password) {
35+
password = this.$prompter.getPassword("Apple ID password").wait();
36+
}
37+
38+
let iOSApplication = this.$itmsTransporterService.getiOSApplication(username, password, bundleId).wait();
39+
40+
if(!mobileProvisionIdentifier && !ipaFilePath) {
41+
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");
42+
}
43+
44+
if(!codeSignIdentity && !ipaFilePath) {
45+
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");
46+
}
47+
48+
this.$options.release = true;
49+
this.$itmsTransporterService.upload({
50+
appId: iOSApplication.adamId,
51+
username,
52+
password,
53+
mobileProvisionIdentifier,
54+
codeSignIdentity,
55+
ipaFilePath,
56+
verboseLogging: this.$logger.getLevel() === "TRACE"
57+
}).wait();
58+
}).future<void>()();
59+
}
60+
61+
public canExecute(args: string[]): IFuture<boolean> {
62+
return (() => {
63+
if (!this.$hostInfo.isDarwin) {
64+
this.$errors.failWithoutHelp("This command is only available on Mac OS X.");
65+
}
66+
67+
return true;
68+
}).future<boolean>()();
69+
}
70+
}
71+
$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

+72
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,77 @@ 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 application's Apple ID. It can be found on itunesconnect.apple.com.
106+
* @type {string}
107+
*/
108+
appId: string;
109+
/**
110+
* The identifier of the mobile provision used for building. Note that this will override the same option set through .xcconfig files.
111+
* @type {string}
112+
*/
113+
mobileProvisionIdentifier?: string;
114+
/**
115+
* The Code Sign Identity used for building. Note that this will override the same option set through .xcconfig files.
116+
* @type {string}
117+
*/
118+
codeSignIdentity?: string;
119+
/**
120+
* Path to a .ipa file which will be uploaded. If set that .ipa will be used and no build will be issued.
121+
* @type {string}
122+
*/
123+
ipaFilePath?: string;
124+
/**
125+
* Specifies whether the logging level of the itmstransporter command-line tool should be set to verbose.
126+
* @type {string}
127+
*/
128+
verboseLogging?: boolean;
129+
}
130+
131+
/**
132+
* Used for communicating with Xcode's iTMS Transporter tool.
133+
*/
134+
interface IITMSTransporterService {
135+
/**
136+
* Uploads an .ipa package to iTunes Connect.
137+
* @param {IITMSData} data Data needed to upload the package
138+
* @return {IFuture<void>}
139+
*/
140+
upload(data: IITMSData): IFuture<void>;
141+
/**
142+
* Queries Apple's content delivery API to get the user's registered iOS applications.
143+
* @param {ICredentials} credentials Credentials for authentication with iTunes Connect.
144+
* @return {IFuture<IItunesConnectApplication[]>} The user's iOS applications.
145+
*/
146+
getiOSApplications(credentials: ICredentials): IFuture<IiTunesConnectApplication[]>;
147+
/**
148+
* Gets iTunes Connect application corresponding to the given bundle identifier.
149+
* @param {string} username For authentication with iTunes Connect.
150+
* @param {string} password For authentication with iTunes Connect.
151+
* @param {string} bundleId Application's Bundle Identifier
152+
* @return {IFuture<IiTunesConnectApplication>} The iTunes Connect application.
153+
*/
154+
getiOSApplication(username: string, password: string, bundleId: string) : IFuture<IiTunesConnectApplication>;
155+
/**
156+
* Gets the application's bundle identifier. If ipaFileFullPath is provided will extract the bundle identifier from the .ipa file.
157+
* @param {string} ipaFileFullPath Optional full path to .ipa file
158+
* @return {IFuture<string>} Application's bundle identifier.
159+
*/
160+
getBundleIdentifier(ipaFileFullPath?: string): IFuture<string>;
161+
}
162+
91163
/**
92164
* Provides access to information about installed Android tools and SDKs versions.
93165
*/

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)